Bug 1434706 - Add identity.fxaccounts.enabled pref to disable Sync and FxA. r=markh
authorEdouard Oger <eoger@fastmail.com>
Thu, 15 Feb 2018 11:24:44 +0800
changeset 405418 109cd0a34ffea65f7540dbd6d7feb9c6ca39ec55
parent 405417 62d9bbdd6feeaec0153ed7ab65a14c77b7dcd032
child 405419 f0a0ac0e2c21b0a00842e30a603c8476ba3b89c6
push id33519
push useraiakab@mozilla.com
push dateTue, 27 Feb 2018 09:56:16 +0000
treeherdermozilla-central@c4425fcdfb5b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmarkh
bugs1434706
milestone60.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 1434706 - Add identity.fxaccounts.enabled pref to disable Sync and FxA. r=markh MozReview-Commit-ID: 4UuppJyOi5s
browser/app/profile/firefox.js
browser/base/content/browser-context.inc
browser/base/content/browser-menubar.inc
browser/base/content/browser-sync.js
browser/base/content/browser.xul
browser/base/content/test/sync/browser_contextmenu_sendpage.js
browser/base/content/test/sync/browser_contextmenu_sendtab.js
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/content/panelUI.inc.xul
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/main.xul
browser/components/preferences/in-content/preferences.js
browser/components/preferences/in-content/preferences.xul
browser/extensions/onboarding/bootstrap.js
browser/extensions/onboarding/content/onboarding.js
browser/modules/PageActions.jsm
services/fxaccounts/FxAccounts.jsm
services/sync/Weave.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1411,16 +1411,20 @@ pref("geo.provider.use_gpsd", true);
 pref("network.disable.ipc.security", true);
 
 // CustomizableUI debug logging.
 pref("browser.uiCustomization.debug", false);
 
 // CustomizableUI state of the browser's user interface
 pref("browser.uiCustomization.state", "");
 
+// If set to false, FxAccounts and Sync will be unavailable.
+// A restart is mandatory after flipping that preference.
+pref("identity.fxaccounts.enabled", true);
+
 // The remote FxA root content URL. Must use HTTPS.
 pref("identity.fxaccounts.remote.root", "https://accounts.firefox.com/");
 
 // The value of the context query parameter passed in fxa requests.
 pref("identity.fxaccounts.contextParam", "fx_desktop_v3");
 
 // 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-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -258,21 +258,23 @@
                 label="&hidePluginCmd.label;"
                 accesskey="&hidePluginCmd.accesskey;"
                 oncommand="gContextMenu.hidePlugin();"/>
       <menuseparator id="context-sep-ctp"/>
       <menuitem id="context-savepage"
                 label="&savePageCmd.label;"
                 accesskey="&savePageCmd.accesskey2;"
                 oncommand="gContextMenu.savePageAs();"/>
-      <menuseparator id="context-sep-sendpagetodevice" hidden="true"/>
+      <menuseparator id="context-sep-sendpagetodevice" class="sync-ui-item"
+                     hidden="true"/>
       <menu id="context-sendpagetodevice"
-                label="&sendPageToDevice.label;"
-                accesskey="&sendPageToDevice.accesskey;"
-                hidden="true">
+            class="sync-ui-item"
+            label="&sendPageToDevice.label;"
+            accesskey="&sendPageToDevice.accesskey;"
+            hidden="true">
         <menupopup id="context-sendpagetodevice-popup"
                    onpopupshowing="(() => { let browser = gBrowser || getPanelBrowser(); gSync.populateSendTabToDevicesMenu(event.target, browser.currentURI.spec, browser.contentTitle); })()"/>
       </menu>
       <menuseparator id="context-sep-viewbgimage"/>
       <menuitem id="context-viewbgimage"
                 label="&viewBGImageCmd.label;"
                 accesskey="&viewBGImageCmd.accesskey;"
                 oncommand="gContextMenu.viewBGImage(event);"
@@ -305,21 +307,23 @@
                 command="cmd_selectAll"/>
       <menuseparator id="context-sep-selectall"/>
       <menuitem id="context-keywordfield"
                 label="&keywordfield.label;"
                 accesskey="&keywordfield.accesskey;"
                 oncommand="AddKeywordForSearchField();"/>
       <menuitem id="context-searchselect"
                 oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
-      <menuseparator id="context-sep-sendlinktodevice" hidden="true"/>
+      <menuseparator id="context-sep-sendlinktodevice" class="sync-ui-item"
+                     hidden="true"/>
       <menu id="context-sendlinktodevice"
-                label="&sendLinkToDevice.label;"
-                accesskey="&sendLinkToDevice.accesskey;"
-                hidden="true">
+            class="sync-ui-item"
+            label="&sendLinkToDevice.label;"
+            accesskey="&sendLinkToDevice.accesskey;"
+            hidden="true">
         <menupopup id="context-sendlinktodevice-popup"
                    onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, gContextMenu.linkURL, gContextMenu.linkTextStr);"/>
       </menu>
       <menuseparator id="frame-sep"/>
       <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
         <menupopup>
           <menuitem id="context-showonlythisframe"
                     label="&showOnlyThisFrameCmd.label;"
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -481,31 +481,35 @@
               <menuitem id="menu_openAddons"
                         label="&addons.label;"
                         accesskey="&addons.accesskey;"
                         key="key_openAddons"
                         command="Tools:Addons"/>
 
               <!-- only one of sync-setup, sync-unverifieditem, sync-syncnowitem or sync-reauthitem will be showing at once -->
               <menuitem id="sync-setup"
+                        class="sync-ui-item"
                         label="&syncSignIn.label;"
                         accesskey="&syncSignIn.accesskey;"
                         observes="sync-setup-state"
                         oncommand="gSync.openPrefs('menubar')"/>
               <menuitem id="sync-unverifieditem"
+                        class="sync-ui-item"
                         label="&syncSignIn.label;"
                         accesskey="&syncSignIn.accesskey;"
                         observes="sync-unverified-state"
                         oncommand="gSync.openPrefs('menubar')"/>
               <menuitem id="sync-syncnowitem"
+                        class="sync-ui-item"
                         label="&syncSyncNowItem.label;"
                         accesskey="&syncSyncNowItem.accesskey;"
                         observes="sync-syncnow-state"
                         oncommand="gSync.doSync(event);"/>
               <menuitem id="sync-reauthitem"
+                        class="sync-ui-item"
                         label="&syncReAuthItem.label;"
                         accesskey="&syncReAuthItem.accesskey;"
                         observes="sync-reauth-state"
                         oncommand="gSync.openSignInAgainPage('menubar');"/>
               <menuseparator id="devToolsSeparator"/>
               <menu id="webDeveloperMenu"
                     label="&webDeveloperMenu.label;"
                     accesskey="&webDeveloperMenu.accesskey;">
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -90,16 +90,18 @@ var gSync = {
             return new RegExp(rx, "i");
           } catch (e) {
             Cu.reportError(`Failed to build url filter regexp for send tab: ${e}`);
             return null;
           }
         });
     XPCOMUtils.defineLazyPreferenceGetter(this, "PRODUCT_INFO_BASE_URL",
         "app.productInfo.baseURL");
+    XPCOMUtils.defineLazyPreferenceGetter(this, "SYNC_ENABLED",
+        "identity.fxaccounts.enabled");
   },
 
   _maybeUpdateUIState() {
     // Update the UI.
     if (UIState.isReady()) {
       const state = UIState.get();
       // If we are not configured, the UI is already in the right state when
       // we open the window. We can avoid a repaint.
@@ -109,16 +111,23 @@ var gSync = {
     }
   },
 
   init() {
     if (this._initialized) {
       return;
     }
 
+    this._definePrefGetters();
+
+    if (!this.SYNC_ENABLED) {
+      this.onSyncDisabled();
+      return;
+    }
+
     // initial label for the sync buttons.
     let statusBroadcaster = document.getElementById("sync-status");
     if (!statusBroadcaster) {
       // We are in a window without our elements - just abort now, without
       // setting this._initialized, so we don't attempt to remove observers.
       return;
     }
     statusBroadcaster.setAttribute("label", this.syncStrings.GetStringFromName("syncnow.label"));
@@ -127,17 +136,16 @@ var gSync = {
     let setupBroadcaster = document.getElementById("sync-setup-state");
     setupBroadcaster.hidden = false;
 
     for (let topic of this._obs) {
       Services.obs.addObserver(this, topic, true);
     }
 
     this._generateNodeGetters();
-    this._definePrefGetters();
 
     this._maybeUpdateUIState();
 
     EnsureFxAccountsWebChannel();
 
     this._initialized = true;
   },
 
@@ -465,24 +473,32 @@ var gSync = {
     // as a valid URI. We've already logged an error when trying to construct
     // the regexp, and the more problematic case is the length, which we've
     // already addressed.
     return true;
   },
 
   // "Send Tab to Device" menu item
   updateTabContextMenu(aPopupMenu, aTargetTab) {
+    if (!this.SYNC_ENABLED) {
+      // These items are hidden in onSyncDisabled(). No need to do anything.
+      return;
+    }
     const enabled = !this.syncConfiguredAndLoading &&
                     this.isSendableURI(aTargetTab.linkedBrowser.currentURI.spec);
 
     document.getElementById("context_sendTabToDevice").disabled = !enabled;
   },
 
   // "Send Page to Device" and "Send Link to Device" menu items
   updateContentContextMenu(contextMenu) {
+    if (!this.SYNC_ENABLED) {
+      // These items are hidden by default. No need to do anything.
+      return;
+    }
     // showSendLink and showSendPage are mutually exclusive
     const showSendLink = contextMenu.onSaveableLink || contextMenu.onPlainTextLink;
     const showSendPage = !showSendLink
                          && !(contextMenu.isContentSelected ||
                               contextMenu.onImage || contextMenu.onCanvas ||
                               contextMenu.onVideo || contextMenu.onAudio ||
                               contextMenu.onLink || contextMenu.onTextInput);
 
@@ -646,13 +662,20 @@ var gSync = {
       if (Weave.Service.clientsEngine.stats.numClients > 1) {
         broadcaster.setAttribute("devices-status", "multi");
       } else {
         broadcaster.setAttribute("devices-status", "single");
       }
     }
   },
 
+  onSyncDisabled() {
+    const toHide = [...document.querySelectorAll(".sync-ui-item")];
+    for (const item of toHide) {
+      item.hidden = true;
+    }
+  },
+
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsIObserver,
     Ci.nsISupportsWeakReference
   ])
 };
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -92,18 +92,19 @@
                 oncommand="gBrowser.unpinTab(TabContextMenu.contextTab);"/>
       <menuitem id="context_duplicateTab" label="&duplicateTab.label;"
                 accesskey="&duplicateTab.accesskey;"
                 oncommand="duplicateTabIn(TabContextMenu.contextTab, 'tab');"/>
       <menuitem id="context_openTabInWindow" label="&moveToNewWindow.label;"
                 accesskey="&moveToNewWindow.accesskey;"
                 tbattr="tabbrowser-multiple"
                 oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
-      <menuseparator id="context_sendTabToDevice_separator"/>
+      <menuseparator id="context_sendTabToDevice_separator" class="sync-ui-item"/>
       <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
+            class="sync-ui-item"
             accesskey="&sendTabToDevice.accesskey;">
         <menupopup id="context_sendTabToDevicePopupMenu"
                    onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
       </menu>
       <menuseparator/>
       <menuitem id="context_reloadAllTabs" label="&reloadAllTabs.label;" accesskey="&reloadAllTabs.accesskey;"
                 tbattr="tabbrowser-multiple-visible"
                 oncommand="gBrowser.reloadAllTabs();"/>
--- a/browser/base/content/test/sync/browser_contextmenu_sendpage.js
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -2,16 +2,18 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const remoteClientsFixture = [ { id: 1, name: "Foo"}, { id: 2, name: "Bar"} ];
 
 add_task(async function setup() {
   await promiseSyncReady();
+  // gSync.init() is called in a requestIdleCallback. Force its initialization.
+  gSync.init();
   await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
 });
 
 add_task(async function test_page_contextmenu() {
   const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: remoteClientsFixture,
                                       state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
@@ -23,17 +25,17 @@ add_task(async function test_page_contex
     "----",
     { label: "Send to All Devices" }
   ]);
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
-add_task(async function test_page_contextmenu_sendtab_no_remote_clients() {
+add_task(async function test_page_contextmenu_no_remote_clients() {
   const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: [],
                                       state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
     { label: "No Devices Connected", disabled: true },
@@ -41,17 +43,17 @@ add_task(async function test_page_contex
     { label: "Connect Another Device..." },
     { label: "Learn About Sending Tabs..." }
   ]);
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
-add_task(async function test_page_contextmenu_sendtab_one_remote_client() {
+add_task(async function test_page_contextmenu_one_remote_client() {
   const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: [{ id: 1, name: "Foo"}],
                                       state: UIState.STATUS_SIGNED_IN, isSendableURI: true });
 
   await openContentContextMenu("#moztext", "context-sendpagetodevice");
   is(document.getElementById("context-sendpagetodevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context-sendpagetodevice").disabled, false, "Send tab to device is enabled");
   checkPopup([
     { label: "Foo" }
@@ -170,16 +172,27 @@ add_task(async function test_page_contex
 
   await hideContentContextMenu();
 
   syncReady.restore();
   getState.restore();
   isSendableURI.restore();
 });
 
+add_task(async function test_page_contextmenu_fxa_disabled() {
+  const getter = sinon.stub(gSync, "SYNC_ENABLED").get(() => false);
+  gSync.onSyncDisabled(); // Would have been called on gSync initialization if SYNC_ENABLED had been set.
+  await openContentContextMenu("#moztext");
+  is(document.getElementById("context-sendpagetodevice").hidden, true, "Send tab to device is hidden");
+  is(document.getElementById("context-sep-sendpagetodevice").hidden, true, "Separator is also hidden");
+  await hideContentContextMenu();
+  getter.restore();
+  [...document.querySelectorAll(".sync-ui-item")].forEach(e => e.hidden = false);
+});
+
 // We are not going to bother testing the visibility of context-sendlinktodevice
 // since it uses the exact same code.
 // However, browser_contextmenu.js contains tests that verify its presence.
 
 add_task(async function teardown() {
   gBrowser.removeCurrentTab();
 });
 
--- a/browser/base/content/test/sync/browser_contextmenu_sendtab.js
+++ b/browser/base/content/test/sync/browser_contextmenu_sendtab.js
@@ -19,16 +19,18 @@ function updateTabContextMenu(tab) {
   tab.dispatchEvent(evt);
   menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
   is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
   menu.hidePopup();
 }
 
 add_task(async function setup() {
   await promiseSyncReady();
+  // gSync.init() is called in a requestIdleCallback. Force its initialization.
+  gSync.init();
   is(gBrowser.visibleTabs.length, 1, "there is one visible tab");
 });
 
 // We are not testing the devices popup contents, since it is already tested by
 // browser_contextmenu_sendpage.js and the code to populate it is the same.
 
 add_task(async function test_tab_contextmenu() {
   const sandbox = setupSendTabMocks({ syncReady: true, clientsSynced: true, remoteClients: remoteClientsFixture,
@@ -90,8 +92,21 @@ add_task(async function test_tab_context
                                       state: UIState.STATUS_NOT_VERIFIED, isSendableURI: true });
 
   updateTabContextMenu(testTab);
   is(document.getElementById("context_sendTabToDevice").hidden, false, "Send tab to device is shown");
   is(document.getElementById("context_sendTabToDevice").disabled, false, "Send tab to device is enabled");
 
   sandbox.restore();
 });
+
+add_task(async function test_tab_contextmenu_fxa_disabled() {
+  const getter = sinon.stub(gSync, "SYNC_ENABLED").get(() => false);
+  // Simulate onSyncDisabled() being called on window open.
+  gSync.onSyncDisabled();
+
+  updateTabContextMenu(testTab);
+  is(document.getElementById("context_sendTabToDevice").hidden, true, "Send tab to device is hidden");
+  is(document.getElementById("context_sendTabToDevice_separator").hidden, true, "Separator is also hidden");
+
+  getter.restore();
+  [...document.querySelectorAll(".sync-ui-item")].forEach(e => e.hidden = false);
+});
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -234,262 +234,16 @@ const CustomizableWidgets = [
         } else {
           element.classList.add("subviewbutton-iconic", "bookmark-item");
         }
       }
       panelview.appendChild(body);
       panelview.appendChild(footer);
     }
   }, {
-    id: "sync-button",
-    label: "remotetabs-panelmenu.label",
-    tooltiptext: "remotetabs-panelmenu.tooltiptext2",
-    type: "view",
-    viewId: "PanelUI-remotetabs",
-    deckIndices: {
-      DECKINDEX_TABS: 0,
-      DECKINDEX_TABSDISABLED: 1,
-      DECKINDEX_FETCHING: 2,
-      DECKINDEX_NOCLIENTS: 3,
-    },
-    TABS_PER_PAGE: 25,
-    NEXT_PAGE_MIN_TABS: 5, // Minimum number of tabs displayed when we click "Show All"
-    onCreated(aNode) {
-      this._initialize(aNode);
-    },
-    _initialize(aNode) {
-      if (this._initialized) {
-        return;
-      }
-      // Add an observer to the button so we get the animation during sync.
-      // (Note the observer sets many attributes, including label and
-      // tooltiptext, but we only want the 'syncstatus' attribute for the
-      // animation)
-      let doc = aNode.ownerDocument;
-      let obnode = doc.createElementNS(kNSXUL, "observes");
-      obnode.setAttribute("element", "sync-status");
-      obnode.setAttribute("attribute", "syncstatus");
-      aNode.appendChild(obnode);
-      this._initialized = true;
-    },
-    onViewShowing(aEvent) {
-      this._initialize(aEvent.target);
-      let doc = aEvent.target.ownerDocument;
-      this._tabsList = doc.getElementById("PanelUI-remotetabs-tabslist");
-      Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
-
-      if (SyncedTabs.isConfiguredToSyncTabs) {
-        if (SyncedTabs.hasSyncedThisSession) {
-          this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
-        } else {
-          // Sync hasn't synced tabs yet, so show the "fetching" panel.
-          this.setDeckIndex(this.deckIndices.DECKINDEX_FETCHING);
-        }
-        // force a background sync.
-        SyncedTabs.syncTabs().catch(ex => {
-          Cu.reportError(ex);
-        });
-        // show the current list - it will be updated by our observer.
-        this._showTabs();
-      } else {
-        // not configured to sync tabs, so no point updating the list.
-        this.setDeckIndex(this.deckIndices.DECKINDEX_TABSDISABLED);
-      }
-    },
-    onViewHiding() {
-      Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
-      this._tabsList = null;
-    },
-    _tabsList: null,
-    observe(subject, topic, data) {
-      switch (topic) {
-        case SyncedTabs.TOPIC_TABS_CHANGED:
-          this._showTabs();
-          break;
-        default:
-          break;
-      }
-    },
-    setDeckIndex(index) {
-      let deck = this._tabsList.ownerDocument.getElementById("PanelUI-remotetabs-deck");
-      // We call setAttribute instead of relying on the XBL property setter due
-      // to things going wrong when we try and set the index before the XBL
-      // binding has been created - see bug 1241851 for the gory details.
-      deck.setAttribute("selectedIndex", index);
-    },
-
-    _showTabsPromise: Promise.resolve(),
-    // Update the tab list after any existing in-flight updates are complete.
-    _showTabs(paginationInfo) {
-      this._showTabsPromise = this._showTabsPromise.then(() => {
-        return this.__showTabs(paginationInfo);
-      }, e => {
-        Cu.reportError(e);
-      });
-    },
-    // Return a new promise to update the tab list.
-    __showTabs(paginationInfo) {
-      if (!this._tabsList) {
-        // Closed between the previous `this._showTabsPromise`
-        // resolving and now.
-        return undefined;
-      }
-      let doc = this._tabsList.ownerDocument;
-      return SyncedTabs.getTabClients().then(clients => {
-        // The view may have been hidden while the promise was resolving.
-        if (!this._tabsList) {
-          return;
-        }
-        if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) {
-          // the "fetching tabs" deck is being shown - let's leave it there.
-          // When that first sync completes we'll be notified and update.
-          return;
-        }
-
-        if (clients.length === 0) {
-          this.setDeckIndex(this.deckIndices.DECKINDEX_NOCLIENTS);
-          return;
-        }
-
-        this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
-        this._clearTabList();
-        SyncedTabs.sortTabClientsByLastUsed(clients);
-        let fragment = doc.createDocumentFragment();
-
-        for (let client of clients) {
-          // add a menu separator for all clients other than the first.
-          if (fragment.lastChild) {
-            let separator = doc.createElementNS(kNSXUL, "menuseparator");
-            fragment.appendChild(separator);
-          }
-          if (paginationInfo && paginationInfo.clientId == client.id) {
-            this._appendClient(client, fragment, paginationInfo.maxTabs);
-          } else {
-            this._appendClient(client, fragment);
-          }
-        }
-        this._tabsList.appendChild(fragment);
-        PanelView.forNode(this._tabsList.closest("panelview"))
-                 .descriptionHeightWorkaround();
-      }).catch(err => {
-        Cu.reportError(err);
-      }).then(() => {
-        // an observer for tests.
-        Services.obs.notifyObservers(null, "synced-tabs-menu:test:tabs-updated");
-      });
-    },
-    _clearTabList() {
-      let list = this._tabsList;
-      while (list.lastChild) {
-        list.lastChild.remove();
-      }
-    },
-    _showNoClientMessage() {
-      this._appendMessageLabel("notabslabel");
-    },
-    _appendMessageLabel(messageAttr, appendTo = null) {
-      if (!appendTo) {
-        appendTo = this._tabsList;
-      }
-      let message = this._tabsList.getAttribute(messageAttr);
-      let doc = this._tabsList.ownerDocument;
-      let messageLabel = doc.createElementNS(kNSXUL, "label");
-      messageLabel.textContent = message;
-      appendTo.appendChild(messageLabel);
-      return messageLabel;
-    },
-    _appendClient(client, attachFragment, maxTabs = this.TABS_PER_PAGE) {
-      let doc = attachFragment.ownerDocument;
-      // Create the element for the remote client.
-      let clientItem = doc.createElementNS(kNSXUL, "label");
-      clientItem.setAttribute("itemtype", "client");
-      let window = doc.defaultView;
-      clientItem.setAttribute("tooltiptext",
-        window.gSync.formatLastSyncDate(new Date(client.lastModified)));
-      clientItem.textContent = client.name;
-
-      attachFragment.appendChild(clientItem);
-
-      if (client.tabs.length == 0) {
-        let label = this._appendMessageLabel("notabsforclientlabel", attachFragment);
-        label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
-      } else {
-        // If this page will display all tabs, show no additional buttons.
-        // If the next page will display all the remaining tabs, show a "Show All" button
-        // Otherwise, show a "Shore More" button
-        let hasNextPage = client.tabs.length > maxTabs;
-        let nextPageIsLastPage = hasNextPage && maxTabs + this.TABS_PER_PAGE >= client.tabs.length;
-        if (nextPageIsLastPage) {
-          // When the user clicks "Show All", try to have at least NEXT_PAGE_MIN_TABS more tabs
-          // to display in order to avoid user frustration
-          maxTabs = Math.min(client.tabs.length - this.NEXT_PAGE_MIN_TABS, maxTabs);
-        }
-        if (hasNextPage) {
-          client.tabs = client.tabs.slice(0, maxTabs);
-        }
-        for (let tab of client.tabs) {
-          let tabEnt = this._createTabElement(doc, tab);
-          attachFragment.appendChild(tabEnt);
-        }
-        if (hasNextPage) {
-          let showAllEnt = this._createShowMoreElement(doc, client.id,
-                                                       nextPageIsLastPage ?
-                                                       Infinity :
-                                                       maxTabs + this.TABS_PER_PAGE);
-          attachFragment.appendChild(showAllEnt);
-        }
-      }
-    },
-    _createTabElement(doc, tabInfo) {
-      let item = doc.createElementNS(kNSXUL, "toolbarbutton");
-      let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
-      item.setAttribute("itemtype", "tab");
-      item.setAttribute("class", "subviewbutton");
-      item.setAttribute("targetURI", tabInfo.url);
-      item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url);
-      item.setAttribute("image", tabInfo.icon);
-      item.setAttribute("tooltiptext", tooltipText);
-      // We need to use "click" instead of "command" here so openUILink
-      // respects different buttons (eg, to open in a new tab).
-      item.addEventListener("click", e => {
-        doc.defaultView.openUILink(tabInfo.url, e);
-        if (doc.defaultView.whereToOpenLink(e) != "current") {
-          e.preventDefault();
-          e.stopPropagation();
-        } else {
-          CustomizableUI.hidePanelForNode(item);
-        }
-        BrowserUITelemetry.countSyncedTabEvent("open", "toolbarbutton-subview");
-      });
-      return item;
-    },
-    _createShowMoreElement(doc, clientId, showCount) {
-      let labelAttr, tooltipAttr;
-      if (showCount === Infinity) {
-        labelAttr = "showAllLabel";
-        tooltipAttr = "showAllTooltipText";
-      } else {
-        labelAttr = "showMoreLabel";
-        tooltipAttr = "showMoreTooltipText";
-      }
-      let showAllItem = doc.createElementNS(kNSXUL, "toolbarbutton");
-      showAllItem.setAttribute("itemtype", "showmorebutton");
-      showAllItem.setAttribute("class", "subviewbutton");
-      let label = this._tabsList.getAttribute(labelAttr);
-      showAllItem.setAttribute("label", label);
-      let tooltipText = this._tabsList.getAttribute(tooltipAttr);
-      showAllItem.setAttribute("tooltiptext", tooltipText);
-      showAllItem.addEventListener("click", e => {
-        e.preventDefault();
-        e.stopPropagation();
-        this._showTabs({ clientId, maxTabs: showCount });
-      });
-      return showAllItem;
-    }
-  }, {
     id: "privatebrowsing-button",
     shortcutId: "key_privatebrowsing",
     onCommand(e) {
       let win = e.target.ownerGlobal;
       win.OpenBrowserWindow({private: true});
     }
   }, {
     id: "save-page-button",
@@ -869,16 +623,266 @@ const CustomizableWidgets = [
     id: "email-link-button",
     tooltiptext: "email-link-button.tooltiptext3",
     onCommand(aEvent) {
       let win = aEvent.view;
       win.MailIntegration.sendLinkForBrowser(win.gBrowser.selectedBrowser);
     }
   }];
 
+if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+  CustomizableWidgets.push({
+    id: "sync-button",
+    label: "remotetabs-panelmenu.label",
+    tooltiptext: "remotetabs-panelmenu.tooltiptext2",
+    type: "view",
+    viewId: "PanelUI-remotetabs",
+    deckIndices: {
+      DECKINDEX_TABS: 0,
+      DECKINDEX_TABSDISABLED: 1,
+      DECKINDEX_FETCHING: 2,
+      DECKINDEX_NOCLIENTS: 3,
+    },
+    TABS_PER_PAGE: 25,
+    NEXT_PAGE_MIN_TABS: 5, // Minimum number of tabs displayed when we click "Show All"
+    onCreated(aNode) {
+      this._initialize(aNode);
+    },
+    _initialize(aNode) {
+      if (this._initialized) {
+        return;
+      }
+      // Add an observer to the button so we get the animation during sync.
+      // (Note the observer sets many attributes, including label and
+      // tooltiptext, but we only want the 'syncstatus' attribute for the
+      // animation)
+      let doc = aNode.ownerDocument;
+      let obnode = doc.createElementNS(kNSXUL, "observes");
+      obnode.setAttribute("element", "sync-status");
+      obnode.setAttribute("attribute", "syncstatus");
+      aNode.appendChild(obnode);
+      this._initialized = true;
+    },
+    onViewShowing(aEvent) {
+      this._initialize(aEvent.target);
+      let doc = aEvent.target.ownerDocument;
+      this._tabsList = doc.getElementById("PanelUI-remotetabs-tabslist");
+      Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
+
+      if (SyncedTabs.isConfiguredToSyncTabs) {
+        if (SyncedTabs.hasSyncedThisSession) {
+          this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
+        } else {
+          // Sync hasn't synced tabs yet, so show the "fetching" panel.
+          this.setDeckIndex(this.deckIndices.DECKINDEX_FETCHING);
+        }
+        // force a background sync.
+        SyncedTabs.syncTabs().catch(ex => {
+          Cu.reportError(ex);
+        });
+        // show the current list - it will be updated by our observer.
+        this._showTabs();
+      } else {
+        // not configured to sync tabs, so no point updating the list.
+        this.setDeckIndex(this.deckIndices.DECKINDEX_TABSDISABLED);
+      }
+    },
+    onViewHiding() {
+      Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
+      this._tabsList = null;
+    },
+    _tabsList: null,
+    observe(subject, topic, data) {
+      switch (topic) {
+        case SyncedTabs.TOPIC_TABS_CHANGED:
+          this._showTabs();
+          break;
+        default:
+          break;
+      }
+    },
+    setDeckIndex(index) {
+      let deck = this._tabsList.ownerDocument.getElementById("PanelUI-remotetabs-deck");
+      // We call setAttribute instead of relying on the XBL property setter due
+      // to things going wrong when we try and set the index before the XBL
+      // binding has been created - see bug 1241851 for the gory details.
+      deck.setAttribute("selectedIndex", index);
+    },
+
+    _showTabsPromise: Promise.resolve(),
+    // Update the tab list after any existing in-flight updates are complete.
+    _showTabs(paginationInfo) {
+      this._showTabsPromise = this._showTabsPromise.then(() => {
+        return this.__showTabs(paginationInfo);
+      }, e => {
+        Cu.reportError(e);
+      });
+    },
+    // Return a new promise to update the tab list.
+    __showTabs(paginationInfo) {
+      if (!this._tabsList) {
+        // Closed between the previous `this._showTabsPromise`
+        // resolving and now.
+        return undefined;
+      }
+      let doc = this._tabsList.ownerDocument;
+      return SyncedTabs.getTabClients().then(clients => {
+        // The view may have been hidden while the promise was resolving.
+        if (!this._tabsList) {
+          return;
+        }
+        if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) {
+          // the "fetching tabs" deck is being shown - let's leave it there.
+          // When that first sync completes we'll be notified and update.
+          return;
+        }
+
+        if (clients.length === 0) {
+          this.setDeckIndex(this.deckIndices.DECKINDEX_NOCLIENTS);
+          return;
+        }
+
+        this.setDeckIndex(this.deckIndices.DECKINDEX_TABS);
+        this._clearTabList();
+        SyncedTabs.sortTabClientsByLastUsed(clients);
+        let fragment = doc.createDocumentFragment();
+
+        for (let client of clients) {
+          // add a menu separator for all clients other than the first.
+          if (fragment.lastChild) {
+            let separator = doc.createElementNS(kNSXUL, "menuseparator");
+            fragment.appendChild(separator);
+          }
+          if (paginationInfo && paginationInfo.clientId == client.id) {
+            this._appendClient(client, fragment, paginationInfo.maxTabs);
+          } else {
+            this._appendClient(client, fragment);
+          }
+        }
+        this._tabsList.appendChild(fragment);
+        PanelView.forNode(this._tabsList.closest("panelview"))
+                 .descriptionHeightWorkaround();
+      }).catch(err => {
+        Cu.reportError(err);
+      }).then(() => {
+        // an observer for tests.
+        Services.obs.notifyObservers(null, "synced-tabs-menu:test:tabs-updated");
+      });
+    },
+    _clearTabList() {
+      let list = this._tabsList;
+      while (list.lastChild) {
+        list.lastChild.remove();
+      }
+    },
+    _showNoClientMessage() {
+      this._appendMessageLabel("notabslabel");
+    },
+    _appendMessageLabel(messageAttr, appendTo = null) {
+      if (!appendTo) {
+        appendTo = this._tabsList;
+      }
+      let message = this._tabsList.getAttribute(messageAttr);
+      let doc = this._tabsList.ownerDocument;
+      let messageLabel = doc.createElementNS(kNSXUL, "label");
+      messageLabel.textContent = message;
+      appendTo.appendChild(messageLabel);
+      return messageLabel;
+    },
+    _appendClient(client, attachFragment, maxTabs = this.TABS_PER_PAGE) {
+      let doc = attachFragment.ownerDocument;
+      // Create the element for the remote client.
+      let clientItem = doc.createElementNS(kNSXUL, "label");
+      clientItem.setAttribute("itemtype", "client");
+      let window = doc.defaultView;
+      clientItem.setAttribute("tooltiptext",
+        window.gSync.formatLastSyncDate(new Date(client.lastModified)));
+      clientItem.textContent = client.name;
+
+      attachFragment.appendChild(clientItem);
+
+      if (client.tabs.length == 0) {
+        let label = this._appendMessageLabel("notabsforclientlabel", attachFragment);
+        label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
+      } else {
+        // If this page will display all tabs, show no additional buttons.
+        // If the next page will display all the remaining tabs, show a "Show All" button
+        // Otherwise, show a "Shore More" button
+        let hasNextPage = client.tabs.length > maxTabs;
+        let nextPageIsLastPage = hasNextPage && maxTabs + this.TABS_PER_PAGE >= client.tabs.length;
+        if (nextPageIsLastPage) {
+          // When the user clicks "Show All", try to have at least NEXT_PAGE_MIN_TABS more tabs
+          // to display in order to avoid user frustration
+          maxTabs = Math.min(client.tabs.length - this.NEXT_PAGE_MIN_TABS, maxTabs);
+        }
+        if (hasNextPage) {
+          client.tabs = client.tabs.slice(0, maxTabs);
+        }
+        for (let tab of client.tabs) {
+          let tabEnt = this._createTabElement(doc, tab);
+          attachFragment.appendChild(tabEnt);
+        }
+        if (hasNextPage) {
+          let showAllEnt = this._createShowMoreElement(doc, client.id,
+                                                       nextPageIsLastPage ?
+                                                       Infinity :
+                                                       maxTabs + this.TABS_PER_PAGE);
+          attachFragment.appendChild(showAllEnt);
+        }
+      }
+    },
+    _createTabElement(doc, tabInfo) {
+      let item = doc.createElementNS(kNSXUL, "toolbarbutton");
+      let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
+      item.setAttribute("itemtype", "tab");
+      item.setAttribute("class", "subviewbutton");
+      item.setAttribute("targetURI", tabInfo.url);
+      item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url);
+      item.setAttribute("image", tabInfo.icon);
+      item.setAttribute("tooltiptext", tooltipText);
+      // We need to use "click" instead of "command" here so openUILink
+      // respects different buttons (eg, to open in a new tab).
+      item.addEventListener("click", e => {
+        doc.defaultView.openUILink(tabInfo.url, e);
+        if (doc.defaultView.whereToOpenLink(e) != "current") {
+          e.preventDefault();
+          e.stopPropagation();
+        } else {
+          CustomizableUI.hidePanelForNode(item);
+        }
+        BrowserUITelemetry.countSyncedTabEvent("open", "toolbarbutton-subview");
+      });
+      return item;
+    },
+    _createShowMoreElement(doc, clientId, showCount) {
+      let labelAttr, tooltipAttr;
+      if (showCount === Infinity) {
+        labelAttr = "showAllLabel";
+        tooltipAttr = "showAllTooltipText";
+      } else {
+        labelAttr = "showMoreLabel";
+        tooltipAttr = "showMoreTooltipText";
+      }
+      let showAllItem = doc.createElementNS(kNSXUL, "toolbarbutton");
+      showAllItem.setAttribute("itemtype", "showmorebutton");
+      showAllItem.setAttribute("class", "subviewbutton");
+      let label = this._tabsList.getAttribute(labelAttr);
+      showAllItem.setAttribute("label", label);
+      let tooltipText = this._tabsList.getAttribute(tooltipAttr);
+      showAllItem.setAttribute("tooltiptext", tooltipText);
+      showAllItem.addEventListener("click", e => {
+        e.preventDefault();
+        e.stopPropagation();
+        this._showTabs({ clientId, maxTabs: showCount });
+      });
+      return showAllItem;
+    }
+  });
+}
+
 let preferencesButton = {
   id: "preferences-button",
   onCommand(aEvent) {
     let win = aEvent.target.ownerGlobal;
     win.openPreferences(undefined, {origin: "preferencesButton"});
   }
 };
 if (AppConstants.platform == "win") {
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -172,17 +172,17 @@
         <vbox id="appMenu-addon-banners"/>
         <toolbarbutton class="panel-banner-item"
                        label-update-available="&updateAvailable.panelUI.label;"
                        label-update-manual="&updateManual.panelUI.label;"
                        label-update-restart="&updateRestart.panelUI.label2;"
                        oncommand="PanelUI._onBannerItemSelected(event)"
                        wrap="true"
                        hidden="true"/>
-        <toolbaritem id="appMenu-fxa-container" class="toolbaritem-combined-buttons">
+        <toolbaritem id="appMenu-fxa-container" class="toolbaritem-combined-buttons sync-ui-item">
           <hbox id="appMenu-fxa-status"
                 flex="1"
                 defaultlabel="&fxaSignIn.label;"
                 signedinTooltiptext="&fxaSignedIn.tooltip;"
                 tooltiptext="&fxaSignedIn.tooltip;"
                 errorlabel="&fxaSignInError.label;"
                 unverifiedlabel="&fxaUnverified.label;"
                 onclick="if (event.which == 1) gSync.onMenuPanelCommand();">
@@ -196,17 +196,17 @@
           <toolbarbutton id="appMenu-fxa-icon"
                          class="subviewbutton subviewbutton-iconic"
                          oncommand="gSync.doSync();"
                          closemenu="none">
             <observes element="sync-status" attribute="syncstatus"/>
             <observes element="sync-status" attribute="tooltiptext"/>
           </toolbarbutton>
         </toolbaritem>
-        <toolbarseparator/>
+        <toolbarseparator class="sync-ui-item"/>
         <toolbarbutton id="appMenu-new-window-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&newNavigatorCmd.label;"
                        key="key_newNavigator"
                        command="cmd_newNavigator"/>
         <toolbarbutton id="appMenu-private-window-button"
                        class="subviewbutton subviewbutton-iconic"
                        label="&newPrivateWindow.label;"
@@ -632,17 +632,17 @@
                        closemenu="none"
                        oncommand="PanelUI.showSubView('PanelUI-history', this)"/>
         <toolbarbutton id="appMenu-library-downloads-button"
                        class="subviewbutton subviewbutton-iconic subviewbutton-nav"
                        label="&libraryDownloads.label;"
                        closemenu="none"
                        oncommand="DownloadsSubview.show(this);"/>
         <toolbarbutton id="appMenu-library-remotetabs-button"
-                       class="subviewbutton subviewbutton-iconic subviewbutton-nav"
+                       class="subviewbutton subviewbutton-iconic subviewbutton-nav sync-ui-item"
                        label="&appMenuRemoteTabs.label;"
                        closemenu="none"
                        oncommand="PanelUI.showSubView('PanelUI-remotetabs', this)"/>
         <toolbarseparator hidden="true"/>
         <label value="&appMenuRecentHighlights.label;"
                hidden="true"
                class="subview-subheader"/>
         <toolbaritem id="appMenu-library-recentHighlights"
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -462,16 +462,21 @@ var gMainPane = {
 
       setEventListener("separateProfileMode", "command", gMainPane.separateProfileModeChange);
       let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
       setEventListener("getStarted", "click", gMainPane.onGetStarted);
 
       OS.File.stat(ignoreSeparateProfile).then(() => separateProfileModeCheckbox.checked = false,
         () => separateProfileModeCheckbox.checked = true);
 
+      if (!Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+        document.getElementById("sync-dev-edition-root").hidden = true;
+        return;
+      }
+
       fxAccounts.getSignedInUser().then(data => {
         document.getElementById("getStarted").selectedIndex = data ? 1 : 0;
       })
         .catch(Cu.reportError);
     }
 
     // Initialize the Firefox Updates section.
     let version = AppConstants.MOZ_APP_VERSION_DISPLAY;
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -28,17 +28,17 @@
           data-category="paneGeneral"
           hidden="true">
   <caption><label>&startup.label;</label></caption>
 
 #ifdef MOZ_DEV_EDITION
   <vbox id="separateProfileBox">
     <checkbox id="separateProfileMode"
               label="&separateProfileMode.label;"/>
-    <hbox align="center" class="indent">
+    <hbox id="sync-dev-edition-root" align="center" class="indent">
       <label id="useFirefoxSync">&useFirefoxSync.label;</label>
       <deck id="getStarted">
         <label class="text-link">&getStarted.notloggedin.label;</label>
         <label class="text-link">&getStarted.configured.label;</label>
       </deck>
     </hbox>
   </vbox>
 #endif
--- a/browser/components/preferences/in-content/preferences.js
+++ b/browser/components/preferences/in-content/preferences.js
@@ -50,17 +50,20 @@ document.addEventListener("DOMContentLoa
 function init_all() {
   Preferences.forceEnableInstantApply();
 
   gSubDialog.init();
   register_module("paneGeneral", gMainPane);
   register_module("paneSearch", gSearchPane);
   register_module("panePrivacy", gPrivacyPane);
   register_module("paneContainers", gContainersPane);
-  register_module("paneSync", gSyncPane);
+  if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+    document.getElementById("category-sync").hidden = false;
+    register_module("paneSync", gSyncPane);
+  }
   register_module("paneSearchResults", gSearchResultsPane);
   gSearchResultsPane.init();
   gMainPane.preInit();
 
   let categories = document.getElementById("categories");
   categories.addEventListener("select", event => gotoPref(event.target.value));
 
   document.documentElement.addEventListener("keydown", function(event) {
--- a/browser/components/preferences/in-content/preferences.xul
+++ b/browser/components/preferences/in-content/preferences.xul
@@ -167,16 +167,17 @@
                       data-l10n-attrs="tooltiptext"
                       align="center">
           <image class="category-icon"/>
           <label class="category-name" flex="1" data-l10n-id="pane-privacy-title"></label>
         </richlistitem>
 
         <richlistitem id="category-sync"
                       class="category"
+                      hidden="true"
                       value="paneSync"
                       helpTopic="prefs-weave"
                       data-l10n-id="category-sync"
                       data-l10n-attrs="tooltiptext"
                       align="center">
           <image class="category-icon"/>
           <label class="category-name" flex="1" data-l10n-id="pane-sync-title"></label>
         </richlistitem>
--- a/browser/extensions/onboarding/bootstrap.js
+++ b/browser/extensions/onboarding/bootstrap.js
@@ -93,16 +93,19 @@ let syncTourChecker = {
     if (state.status == UIState.STATUS_NOT_CONFIGURED) {
       this._loggedIn = false;
     } else {
       this.setComplete();
     }
   },
 
   init() {
+    if (!Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+      return;
+    }
     // Check if we've already logged in at startup.
     const state = UIState.get();
     if (state.status != UIState.STATUS_NOT_CONFIGURED) {
       this.setComplete();
     }
     this.register();
   },
 
--- a/browser/extensions/onboarding/content/onboarding.js
+++ b/browser/extensions/onboarding/content/onboarding.js
@@ -474,17 +474,22 @@ class Onboarding {
     this._onIconStateChange(Services.prefs.getStringPref("browser.onboarding.state", ICON_STATE_DEFAULT));
 
     // Doing tour notification takes some effort. Let's do it on idle.
     this._window.requestIdleCallback(() => this.showNotification());
   }
 
   _getTourIDList() {
     let tours = Services.prefs.getStringPref(`browser.onboarding.${this._tourType}tour`, "");
-    return tours.split(",").filter(tourId => tourId !== "").map(tourId => tourId.trim());
+    return tours.split(",").filter(tourId => {
+      if (tourId === "sync" && !Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+        return false;
+      }
+      return tourId !== "";
+    }).map(tourId => tourId.trim());
   }
 
   _initPrefObserver() {
     if (this._prefsObserved) {
       return;
     }
 
     this._prefsObserved = new Map();
--- a/browser/modules/PageActions.jsm
+++ b/browser/modules/PageActions.jsm
@@ -1213,18 +1213,21 @@ var gBuiltInActions = [
     id: "emailLink",
     title: "emailLink-title",
     onPlacedInPanel(buttonNode) {
       browserPageActions(buttonNode).emailLink.onPlacedInPanel(buttonNode);
     },
     onCommand(event, buttonNode) {
       browserPageActions(buttonNode).emailLink.onCommand(event, buttonNode);
     },
-  },
+  }
+];
 
+if (Services.prefs.getBoolPref("identity.fxaccounts.enabled")) {
+  gBuiltInActions.push(
   // send to device
   {
     id: "sendToDevice",
     title: "sendToDevice-title",
     onPlacedInPanel(buttonNode) {
       browserPageActions(buttonNode).sendToDevice.onPlacedInPanel(buttonNode);
     },
     onLocationChange(browserWindow) {
@@ -1242,18 +1245,18 @@ var gBuiltInActions = [
         browserPageActions(panelViewNode).sendToDevice
           .onSubviewPlaced(panelViewNode);
       },
       onShowing(panelViewNode) {
         browserPageActions(panelViewNode).sendToDevice
           .onShowingSubview(panelViewNode);
       },
     },
-  }
-];
+  });
+}
 
 
 /**
  * Gets a BrowserPageActions object in a browser window.
  *
  * @param  obj
  *         Either a DOM node or a browser window.
  * @return The BrowserPageActions object in the browser window related to the
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -31,16 +31,19 @@ ChromeUtils.defineModuleGetter(this, "Fx
   "resource://gre/modules/FxAccountsOAuthGrantClient.jsm");
 
 ChromeUtils.defineModuleGetter(this, "FxAccountsProfile",
   "resource://gre/modules/FxAccountsProfile.jsm");
 
 ChromeUtils.defineModuleGetter(this, "Utils",
   "resource://services-sync/util.js");
 
+XPCOMUtils.defineLazyPreferenceGetter(this, "FXA_ENABLED",
+    "identity.fxaccounts.enabled", true);
+
 // All properties exposed by the public FxAccounts API.
 var publicProperties = [
   "accountStatus",
   "canGetKeys",
   "checkVerificationStatus",
   "getAccountsClient",
   "getAssertion",
   "getDeviceId",
@@ -516,30 +519,33 @@ FxAccountsInternal.prototype = {
    *          kExtSync: An encryption key for WebExtensions syncing
    *          kExtKbHash: A key hash of kB for WebExtensions syncing
    *          verified: email verification status
    *          authAt: The time (seconds since epoch) that this record was
    *                  authenticated
    *        }
    *        or null if no user is signed in.
    */
-  getSignedInUser: function getSignedInUser() {
+  async getSignedInUser() {
     let currentState = this.currentAccountState;
-    return currentState.getUserAccountData().then(data => {
-      if (!data) {
-        return null;
-      }
-      if (!this.isUserEmailVerified(data)) {
-        // If the email is not verified, start polling for verification,
-        // but return null right away.  We don't want to return a promise
-        // that might not be fulfilled for a long time.
-        this.startVerifiedCheck(data);
-      }
-      return data;
-    }).then(result => currentState.resolve(result));
+    const data = await currentState.getUserAccountData();
+    if (!data) {
+      return currentState.resolve(null);
+    }
+    if (!FXA_ENABLED) {
+      await this.signOut();
+      return currentState.resolve(null);
+    }
+    if (!this.isUserEmailVerified(data)) {
+      // If the email is not verified, start polling for verification,
+      // but return null right away.  We don't want to return a promise
+      // that might not be fulfilled for a long time.
+      this.startVerifiedCheck(data);
+    }
+    return currentState.resolve(data);
   },
 
   /**
    * Set the current user signed in to Firefox Accounts.
    *
    * @param credentials
    *        The credentials object obtained by logging in or creating
    *        an account on the FxA server:
@@ -553,47 +559,42 @@ FxAccountsInternal.prototype = {
    *          unwrapBKey: used to unwrap kB, derived locally from the
    *                      password (not revealed to the FxA server)
    *          verified: true/false
    *        }
    * @return Promise
    *         The promise resolves to null when the data is saved
    *         successfully and is rejected on error.
    */
-  setSignedInUser: function setSignedInUser(credentials) {
+  async setSignedInUser(credentials) {
+    if (!FXA_ENABLED) {
+      throw new Error("Cannot call setSignedInUser when FxA is disabled.");
+    }
     log.debug("setSignedInUser - aborting any existing flows");
-    return this.getSignedInUser().then(signedInUser => {
-      if (signedInUser) {
-        return this.deleteDeviceRegistration(signedInUser.sessionToken, signedInUser.deviceId);
-      }
-      return null;
-    }).then(() =>
-      this.abortExistingFlow()
-    ).then(() => {
-      let currentAccountState = this.currentAccountState = this.newAccountState(
-        Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object.
-      );
-      // This promise waits for storage, but not for verification.
-      // We're telling the caller that this is durable now (although is that
-      // really something we should commit to? Why not let the write happen in
-      // the background? Already does for updateAccountData ;)
-      return currentAccountState.promiseInitialized.then(() => {
-        // Starting point for polling if new user
-        if (!this.isUserEmailVerified(credentials)) {
-          this.startVerifiedCheck(credentials);
-        }
-
-        return this.updateDeviceRegistration();
-      }).then(() => {
-        Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1);
-        return this.notifyObservers(ONLOGIN_NOTIFICATION);
-      }).then(() => {
-        return currentAccountState.resolve();
-      });
-    });
+    const signedInUser = await this.getSignedInUser();
+    if (signedInUser) {
+      await this.deleteDeviceRegistration(signedInUser.sessionToken, signedInUser.deviceId);
+    }
+    await this.abortExistingFlow();
+    let currentAccountState = this.currentAccountState = this.newAccountState(
+      Cu.cloneInto(credentials, {}) // Pass a clone of the credentials object.
+    );
+    // This promise waits for storage, but not for verification.
+    // We're telling the caller that this is durable now (although is that
+    // really something we should commit to? Why not let the write happen in
+    // the background? Already does for updateAccountData ;)
+    await currentAccountState.promiseInitialized;
+    // Starting point for polling if new user
+    if (!this.isUserEmailVerified(credentials)) {
+      this.startVerifiedCheck(credentials);
+    }
+    await this.updateDeviceRegistration();
+    Services.telemetry.getHistogramById("FXA_CONFIGURED").add(1);
+    await this.notifyObservers(ONLOGIN_NOTIFICATION);
+    return currentAccountState.resolve();
   },
 
   /**
    * Update account data for the currently signed in user.
    *
    * @param credentials
    *        The credentials object containing the fields to be updated.
    *        This object must contain the |uid| field and it must
--- a/services/sync/Weave.js
+++ b/services/sync/Weave.js
@@ -112,23 +112,23 @@ WeaveService.prototype = {
         }
       }
     }, 10000, Ci.nsITimer.TYPE_ONE_SHOT);
   },
 
   /**
    * Whether Sync appears to be enabled.
    *
-   * This returns true if we have an associated FxA account
+   * This returns true if we have an associated FxA account and Sync is enabled.
    *
    * It does *not* perform a robust check to see if the client is working.
    * For that, you'll want to check Weave.Status.checkSetup().
    */
   get enabled() {
-    return !!syncUsername;
+    return !!syncUsername && Services.prefs.getBoolPref("identity.fxaccounts.enabled");
   }
 };
 
 function AboutWeaveLog() {}
 AboutWeaveLog.prototype = {
   classID: Components.ID("{d28f8a0b-95da-48f4-b712-caf37097be41}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule,