Bug 1418466 - Add Connect Another Device button to relevant Sync UI. r?markh, dao draft
authorEdouard Oger <eoger@fastmail.com>
Fri, 17 Nov 2017 16:48:17 -0500
changeset 705798 92e2d0a1ac146e21fee2a060a6b0884282ffae92
parent 705606 ba283baf4e98aa3a5f45a17981077b98697aa73a
child 742462 6d011fc6ce2fbccf46a45121af6a809696f7cf49
push id91586
push userbmo:eoger@fastmail.com
push dateThu, 30 Nov 2017 17:40:42 +0000
reviewersmarkh, dao
bugs1418466
milestone59.0a1
Bug 1418466 - Add Connect Another Device button to relevant Sync UI. r?markh, dao MozReview-Commit-ID: 5vBKH9NejVV
browser/app/profile/firefox.js
browser/base/content/browser-sync.js
browser/base/content/test/sync/browser_contextmenu_sendpage.js
browser/base/content/test/urlbar/browser_page_action_menu.js
browser/components/customizableui/CustomizableWidgets.jsm
browser/components/customizableui/content/panelUI.inc.xul
browser/components/customizableui/test/browser_synced_tabs_menu.js
browser/components/syncedtabs/SyncedTabsDeckComponent.js
browser/components/syncedtabs/SyncedTabsDeckView.js
browser/components/syncedtabs/sidebar.xhtml
browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
browser/locales/en-US/chrome/browser/accounts.properties
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/themes/shared/customizableui/panelUI.inc.css
services/fxaccounts/FxAccountsConfig.jsm
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1430,16 +1430,18 @@ pref("identity.fxaccounts.settings.devic
 pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
 
 // The remote URL of the FxA OAuth Server
 pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
 
 // Token server used by the FxA Sync identity.
 pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
 
+// The URL to a page that explains how to connect another device to Sync.
+pref("identity.fxaccounts.remote.connectdevice.uri", "https://accounts.firefox.com/connect_another_device?service=sync&context=fx_desktop_v3");
 // URLs for promo links to mobile browsers. Note that consumers are expected to
 // append a value for utm_campaign.
 pref("identity.mobilepromo.android", "https://www.mozilla.org/firefox/android/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=");
 pref("identity.mobilepromo.ios", "https://www.mozilla.org/firefox/ios/?utm_source=firefox-browser&utm_medium=firefox-browser&utm_campaign=");
 
 // Migrate any existing Firefox Account data from the default profile to the
 // Developer Edition profile.
 #ifdef MOZ_DEV_EDITION
--- a/browser/base/content/browser-sync.js
+++ b/browser/base/content/browser-sync.js
@@ -269,16 +269,22 @@ var gSync = {
   async openDevicesManagementPage(entryPoint) {
     let url = await fxAccounts.promiseAccountsManageDevicesURI(entryPoint);
     switchToTabHavingURI(url, true, {
       replaceQueryString: true,
       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
     });
   },
 
+  openConnectAnotherDevice(entryPoint) {
+    let url = new URL(Services.prefs.getCharPref("identity.fxaccounts.remote.connectdevice.uri"));
+    url.searchParams.append("entrypoint", entryPoint);
+    openUILinkIn(url.href, "tab");
+  },
+
   openSendToDevicePromo() {
     let url = Services.prefs.getCharPref("app.productInfo.baseURL");
     url += "send-tabs/?utm_source=" + Services.appinfo.name.toLowerCase();
     switchToTabHavingURI(url, true, { replaceQueryString: true });
   },
 
   sendTabToDevice(url, clientId, title) {
     Weave.Service.clientsEngine.sendURIToClientForDisplay(url, clientId, title).catch(e => {
@@ -358,67 +364,68 @@ var gSync = {
       const allDevicesLabel = this.fxaStrings.GetStringFromName("sendToAllDevices.menuitem");
       addTargetDevice("", allDevicesLabel, "");
     }
   },
 
   _appendSendTabSingleDevice(fragment, createDeviceNodeFn) {
     const noDevices = this.fxaStrings.GetStringFromName("sendTabToDevice.singledevice.status");
     const learnMore = this.fxaStrings.GetStringFromName("sendTabToDevice.singledevice");
-    this._appendSendTabInfoItems(fragment, createDeviceNodeFn, noDevices, learnMore, () => {
-      this.openSendToDevicePromo();
-    });
+    const connectDevice = this.fxaStrings.GetStringFromName("sendTabToDevice.connectdevice");
+    const actions = [{label: connectDevice, command: () => this.openConnectAnotherDevice("sendtab")},
+                     {label: learnMore,     command: () => this.openSendToDevicePromo()}];
+    this._appendSendTabInfoItems(fragment, createDeviceNodeFn, noDevices, actions);
   },
 
   _appendSendTabVerify(fragment, createDeviceNodeFn) {
     const notVerified = this.fxaStrings.GetStringFromName("sendTabToDevice.verify.status");
     const verifyAccount = this.fxaStrings.GetStringFromName("sendTabToDevice.verify");
-    this._appendSendTabInfoItems(fragment, createDeviceNodeFn, notVerified, verifyAccount, () => {
-      this.openPrefs("sendtab");
-    });
+    const actions = [{label: verifyAccount, command: () => this.openPrefs("sendtab")}];
+    this._appendSendTabInfoItems(fragment, createDeviceNodeFn, notVerified, actions);
   },
 
   _appendSendTabUnconfigured(fragment, createDeviceNodeFn) {
     const notConnected = this.fxaStrings.GetStringFromName("sendTabToDevice.unconfigured.status");
     const learnMore = this.fxaStrings.GetStringFromName("sendTabToDevice.unconfigured");
-    this._appendSendTabInfoItems(fragment, createDeviceNodeFn, notConnected, learnMore, () => {
-      this.openSendToDevicePromo();
-    });
+    const actions = [{label: learnMore, command: () => this.openSendToDevicePromo()}];
+    this._appendSendTabInfoItems(fragment, createDeviceNodeFn, notConnected, actions);
 
     // Now add a 'sign in to sync' item above the 'learn more' item.
     const signInToSync = this.fxaStrings.GetStringFromName("sendTabToDevice.signintosync");
     let signInItem = createDeviceNodeFn(null, signInToSync, null);
     signInItem.classList.add("sync-menuitem");
     signInItem.setAttribute("label", signInToSync);
     // Show an icon if opened in the page action panel:
     if (signInItem.classList.contains("subviewbutton")) {
       signInItem.classList.add("subviewbutton-iconic", "signintosync");
     }
     signInItem.addEventListener("command", () => {
       this.openPrefs("sendtab");
     });
     fragment.insertBefore(signInItem, fragment.lastChild);
   },
 
-  _appendSendTabInfoItems(fragment, createDeviceNodeFn, statusLabel, actionLabel, actionCommand) {
+  _appendSendTabInfoItems(fragment, createDeviceNodeFn, statusLabel, actions) {
     const status = createDeviceNodeFn(null, statusLabel, null);
     status.setAttribute("label", statusLabel);
     status.setAttribute("disabled", true);
     status.classList.add("sync-menuitem");
     fragment.appendChild(status);
 
     const separator = createDeviceNodeFn(null, null, null);
     separator.classList.add("sync-menuitem");
     fragment.appendChild(separator);
 
-    const actionItem = createDeviceNodeFn(null, actionLabel, null);
-    actionItem.addEventListener("command", actionCommand, true);
-    actionItem.classList.add("sync-menuitem");
-    actionItem.setAttribute("label", actionLabel);
-    fragment.appendChild(actionItem);
+    for (let {label, command} of actions) {
+      const actionItem = createDeviceNodeFn(null, label, null);
+      actionItem.addEventListener("command", command, true);
+      actionItem.classList.add("sync-menuitem");
+      actionItem.setAttribute("label", label);
+      fragment.appendChild(actionItem);
+    }
   },
 
   isSendableURI(aURISpec) {
     if (!aURISpec) {
       return false;
     }
     // Disallow sending tabs with more than 65535 characters.
     if (aURISpec.length > 65535) {
--- a/browser/base/content/test/sync/browser_contextmenu_sendpage.js
+++ b/browser/base/content/test/sync/browser_contextmenu_sendpage.js
@@ -33,16 +33,17 @@ add_task(async function test_page_contex
                                       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 },
     "----",
+    { label: "Connect Another Device..." },
     { label: "Learn About Sending Tabs..." }
   ]);
   await hideContentContextMenu();
 
   sandbox.restore();
 });
 
 add_task(async function test_page_contextmenu_sendtab_one_remote_client() {
--- a/browser/base/content/test/urlbar/browser_page_action_menu.js
+++ b/browser/base/content/test/urlbar/browser_page_action_menu.js
@@ -433,16 +433,21 @@ add_task(async function sendToDevice_noD
         attrs: {
           label: "No Devices Connected",
         },
         disabled: true
       },
       null,
       {
         attrs: {
+          label: "Connect Another Device..."
+        }
+      },
+      {
+        attrs: {
           label: "Learn About Sending Tabs..."
         }
       }
     ];
     checkSendToDeviceItems(expectedItems);
 
     // Done, hide the panel.
     let hiddenPromise = promisePageActionPanelHidden();
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -262,51 +262,16 @@ const CustomizableWidgets = [
       // (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);
-
-      // A somewhat complicated dance to format the mobilepromo label.
-      let bundle = doc.getElementById("bundle_browser");
-      let formatArgs = ["android", "ios"].map(os => {
-        let link = doc.createElement("label");
-        link.textContent = bundle.getString(`appMenuRemoteTabs.mobilePromo.${os}`);
-        link.setAttribute("mobile-promo-os", os);
-        link.className = "text-link remotetabs-promo-link";
-        return link.outerHTML;
-      });
-      let promoParentElt = doc.getElementById("PanelUI-remotetabs-mobile-promo");
-      // Put it all together...
-      let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo.text2", formatArgs);
-      // eslint-disable-next-line no-unsanitized/property
-      promoParentElt.innerHTML = contents;
-      // We manually manage the "click" event to open the promo links because
-      // allowing the "text-link" widget handle it has 2 problems: (1) it only
-      // supports button 0 and (2) it's tricky to intercept when it does the
-      // open and auto-close the panel. (1) can probably be fixed, but (2) is
-      // trickier without hard-coding here the knowledge of exactly what buttons
-      // it does support.
-      // So we allow left and middle clicks to open the link in a new tab and
-      // close the panel; not setting a "href" attribute prevents the text-link
-      // widget handling it, and we build the final URL in the click handler to
-      // make testing easier (ie, so tests can change the pref after the links
-      // were created and have the new pref value used.)
-      promoParentElt.addEventListener("click", e => {
-        let os = e.target.getAttribute("mobile-promo-os");
-        if (!os || e.button > 1) {
-          return;
-        }
-        let link = Services.prefs.getCharPref(`identity.mobilepromo.${os}`) + "synced-tabs";
-        doc.defaultView.openUILinkIn(link, "tab");
-        CustomizableUI.hidePanelForNode(e.target);
-      });
       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);
 
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -433,64 +433,66 @@
             <!-- Sync is ready to Sync but the "tabs" engine isn't enabled-->
             <hbox id="PanelUI-remotetabs-tabsdisabledpane" pack="center" flex="1">
               <vbox class="PanelUI-remotetabs-instruction-box" align="center">
                 <hbox pack="center">
                   <image class="fxaSyncIllustrationIssue"/>
                 </hbox>
                 <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.tabsnotsyncing.label;</label>
                 <hbox pack="center">
-                  <toolbarbutton class="PanelUI-remotetabs-prefs-button"
+                  <toolbarbutton class="PanelUI-remotetabs-button"
                                  label="&appMenuRemoteTabs.openprefs.label;"
                                  oncommand="gSync.openPrefs('synced-tabs');"/>
                 </hbox>
               </vbox>
             </hbox>
             <!-- Sync is ready to Sync but we are still fetching the tabs to show -->
             <vbox id="PanelUI-remotetabs-fetching">
               <!-- Show intentionally blank panel, see bug 1239845 -->
             </vbox>
             <!-- Sync has only 1 (ie, this) device connected -->
             <hbox id="PanelUI-remotetabs-nodevicespane" pack="center" flex="1">
               <vbox class="PanelUI-remotetabs-instruction-box">
                 <hbox pack="center">
                   <image class="fxaSyncIllustrationIssue"/>
                 </hbox>
                 <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.noclients.subtitle;</label>
-                <!-- The inner HTML for PanelUI-remotetabs-mobile-promo is built at runtime -->
-                <label id="PanelUI-remotetabs-mobile-promo" fxAccountsBrand="&syncBrand.fxAccount.label;"/>
+                <toolbarbutton id="PanelUI-remotetabs-connect-device-button"
+                               class="PanelUI-remotetabs-button"
+                               label="&appMenuRemoteTabs.connectdevice.label;"
+                               oncommand="gSync.openConnectAnotherDevice('synced-tabs');"/>
               </vbox>
             </hbox>
           </deck>
         </vbox>
         <!-- a box to ensure contained boxes are centered horizonally -->
         <hbox pack="center" flex="1">
           <!-- When Sync is not configured -->
           <vbox id="PanelUI-remotetabs-setupsync"
                 flex="1"
                 align="center"
                 class="PanelUI-remotetabs-instruction-box"
                 observes="sync-setup-state">
             <image class="fxaSyncIllustration"/>
             <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
-            <toolbarbutton class="PanelUI-remotetabs-prefs-button"
+            <toolbarbutton class="PanelUI-remotetabs-button"
                            label="&appMenuRemoteTabs.signin.label;"
                            oncommand="gSync.openPrefs('synced-tabs');"/>
           </vbox>
           <!-- When Sync needs re-authentication. This uses the exact same messaging
                as "Sync is not configured" but remains a separate box so we get
                the goodness of observing broadcasters to manage the hidden states -->
           <vbox id="PanelUI-remotetabs-reauthsync"
                 flex="1"
                 align="center"
                 class="PanelUI-remotetabs-instruction-box"
                 observes="sync-reauth-state">
             <image class="fxaSyncIllustrationIssue"/>
             <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
-            <toolbarbutton class="PanelUI-remotetabs-prefs-button"
+            <toolbarbutton class="PanelUI-remotetabs-button"
                            label="&appMenuRemoteTabs.signin.label;"
                            oncommand="gSync.openPrefs('synced-tabs');"/>
           </vbox>
         </hbox>
       </vbox>
     </panelview>
 
     <panelview id="PanelUI-bookmarks" flex="1" class="PanelUI-subView">
--- a/browser/components/customizableui/test/browser_synced_tabs_menu.js
+++ b/browser/components/customizableui/test/browser_synced_tabs_menu.js
@@ -82,17 +82,17 @@ async function openPrefsFromMenuPanel(ex
   await Promise.all([tabsUpdatedPromise, viewShownPromise]);
   ok(syncPanel.getAttribute("current"), "Sync Panel is in view");
 
   // Sync is not configured - verify that state is reflected.
   let subpanel = document.getElementById(expectedPanelId);
   ok(!subpanel.hidden, "sync setup element is visible");
 
   // Find and click the "setup" button.
-  let setupButton = subpanel.querySelector(".PanelUI-remotetabs-prefs-button");
+  let setupButton = subpanel.querySelector(".PanelUI-remotetabs-button");
   setupButton.click();
 
   await new Promise(resolve => {
     let handler = async (e) => {
       if (e.originalTarget != gBrowser.selectedBrowser.contentDocument ||
           e.target.location.href == "about:blank") {
         info("Skipping spurious 'load' event for " + e.target.location.href);
         return;
@@ -140,69 +140,48 @@ add_task(async function() {
 add_task(asyncCleanup);
 
 // When Sync is configured in a "needs reauthentication" state.
 add_task(async function() {
   gSync.updateAllUI({ status: UIState.STATUS_LOGIN_FAILED, email: "foo@bar.com" });
   await openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs");
 });
 
-// Test the mobile promo links
+// Test the Connect Another Device button
 add_task(async function() {
-  // change the preferences for the mobile links.
-  Services.prefs.setCharPref("identity.mobilepromo.android", "http://example.com/?os=android&tail=");
-  Services.prefs.setCharPref("identity.mobilepromo.ios", "http://example.com/?os=ios&tail=");
+  Services.prefs.setCharPref("identity.fxaccounts.remote.connectdevice.uri", "http://example.com/connectdevice");
 
   gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" });
 
-  let syncPanel = document.getElementById("PanelUI-remotetabs");
-  let links = syncPanel.querySelectorAll(".remotetabs-promo-link");
-
-  is(links.length, 2, "found 2 links as expected");
+  let button = document.getElementById("PanelUI-remotetabs-connect-device-button");
+  ok(button, "found the button");
 
-  // test each link and left and middle mouse buttons
-  for (let link of links) {
-    for (let button = 0; button < 2; button++) {
-      await document.getElementById("nav-bar").overflowable.show();
-      EventUtils.sendMouseEvent({ type: "click", button }, link, window);
-      // the panel should have been closed.
-      ok(!isOverflowOpen(), "click closed the panel");
-      // should be a new tab - wait for the load.
-      is(gBrowser.tabs.length, 2, "there's a new tab");
-      await new Promise(resolve => {
-        if (gBrowser.selectedBrowser.currentURI.spec == "about:blank") {
-          gBrowser.selectedBrowser.addEventListener("load", function(e) {
-            resolve();
-          }, {capture: true, once: true});
-          return;
-        }
-        // the new tab has already transitioned away from about:blank so we
-        // are good to go.
+  await document.getElementById("nav-bar").overflowable.show();
+  button.click();
+  // the panel should have been closed.
+  ok(!isOverflowOpen(), "click closed the panel");
+  // should be a new tab - wait for the load.
+  is(gBrowser.tabs.length, 2, "there's a new tab");
+  await new Promise(resolve => {
+    if (gBrowser.selectedBrowser.currentURI.spec == "about:blank") {
+      gBrowser.selectedBrowser.addEventListener("load", function(e) {
         resolve();
-      });
-
-      let os = link.getAttribute("mobile-promo-os");
-      let expectedUrl = `http://example.com/?os=${os}&tail=synced-tabs`;
-      is(gBrowser.selectedBrowser.currentURI.spec, expectedUrl, "correct URL");
-      gBrowser.removeTab(gBrowser.selectedTab);
+      }, {capture: true, once: true});
+      return;
     }
-  }
+    // the new tab has already transitioned away from about:blank so we
+    // are good to go.
+    resolve();
+  });
 
-  // test each link and right mouse button - should be a noop.
-  await document.getElementById("nav-bar").overflowable.show();
-  for (let link of links) {
-    EventUtils.sendMouseEvent({ type: "click", button: 2 }, link, window);
-    // the panel should still be open
-    ok(isOverflowOpen(), "panel remains open after right-click");
-    is(gBrowser.tabs.length, 1, "no new tab was opened");
-  }
-  await hideOverflow();
+  let expectedUrl = `http://example.com/connectdevice?entrypoint=synced-tabs`;
+  is(gBrowser.selectedBrowser.currentURI.spec, expectedUrl, "correct URL");
+  gBrowser.removeTab(gBrowser.selectedTab);
 
-  Services.prefs.clearUserPref("identity.mobilepromo.android");
-  Services.prefs.clearUserPref("identity.mobilepromo.ios");
+  Services.prefs.clearUserPref("identity.fxaccounts.remote.connectdevice.uri");
 });
 
 // Test the "Sync Now" button
 add_task(async function() {
   gSync.updateAllUI({ status: UIState.STATUS_SIGNED_IN, email: "foo@bar.com" });
 
   await document.getElementById("nav-bar").overflowable.show();
   let tabsUpdatedPromise = promiseObserverNotified("synced-tabs-menu:test:tabs-updated");
--- a/browser/components/syncedtabs/SyncedTabsDeckComponent.js
+++ b/browser/components/syncedtabs/SyncedTabsDeckComponent.js
@@ -74,18 +74,17 @@ SyncedTabsDeckComponent.prototype = {
     Services.obs.addObserver(this, FxAccountsCommon.ONLOGIN_NOTIFICATION);
     Services.obs.addObserver(this, "weave:service:login:change");
 
     // Go ahead and trigger sync
     this._SyncedTabs.syncTabs()
                     .catch(Cu.reportError);
 
     this._deckView = new this._DeckView(this._window, this.tabListComponent, {
-      onAndroidClick: event => this.openAndroidLink(event),
-      oniOSClick: event => this.openiOSLink(event),
+      onConnectDeviceClick: event => this.openConnectDevice(event),
       onSyncPrefClick: event => this.openSyncPrefs(event)
     });
 
     this._deckStore.on("change", state => this._deckView.render(state));
     // Trigger the initial rendering of the deck view
     // Object.values only in nightly
     this._deckStore.setPanels(Object.keys(this.PANELS).map(k => this.PANELS[k]));
     // Set the initial panel to display
@@ -146,27 +145,17 @@ SyncedTabsDeckComponent.prototype = {
 
   updatePanel() {
     // return promise for tests
     return this.getPanelStatus()
       .then(panelId => this._deckStore.selectPanel(panelId))
       .catch(Cu.reportError);
   },
 
-  openAndroidLink(event) {
-    let href = Services.prefs.getCharPref("identity.mobilepromo.android") + "synced-tabs-sidebar";
-    this._openUrl(href, event);
-  },
-
-  openiOSLink(event) {
-    let href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar";
-    this._openUrl(href, event);
+  openSyncPrefs() {
+    this._getChromeWindow(this._window).gSync.openPrefs("tabs-sidebar");
   },
 
-  _openUrl(url, event) {
-    this._window.openUILink(url, event);
+  openConnectDevice() {
+    this._getChromeWindow(this._window).gSync.openConnectAnotherDevice("tabs-sidebar");
   },
-
-  openSyncPrefs() {
-    this._getChromeWindow(this._window).gSync.openPrefs("tabs-sidebar");
-  }
 };
 
--- a/browser/components/syncedtabs/SyncedTabsDeckView.js
+++ b/browser/components/syncedtabs/SyncedTabsDeckView.js
@@ -1,18 +1,16 @@
 /* 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/. */
 
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
-let { getChromeWindow } = Cu.import("resource:///modules/syncedtabs/util.js", {});
-
 let log = Cu.import("resource://gre/modules/Log.jsm", {})
             .Log.repository.getLogger("Sync.RemoteTabs");
 
 this.EXPORTED_SYMBOLS = [
   "SyncedTabsDeckView"
 ];
 
 /**
@@ -49,41 +47,20 @@ SyncedTabsDeckView.prototype = {
 
     let tabListWrapper = this._doc.createElement("div");
     tabListWrapper.className = "tabs-container sync-state";
     this._tabListComponent.init();
     tabListWrapper.appendChild(this._tabListComponent.container);
     deck.appendChild(tabListWrapper);
     this.container.appendChild(deck);
 
-    this._generateDevicePromo();
-
     this._attachListeners();
     this.update(state);
   },
 
-  _getBrowserBundle() {
-    return getChromeWindow(this._window).document.getElementById("bundle_browser");
-  },
-
-  _generateDevicePromo() {
-    let bundle = this._getBrowserBundle();
-    let formatArgs = ["android", "ios"].map(os => {
-      let link = this._doc.createElement("a");
-      link.textContent = bundle.getString(`appMenuRemoteTabs.mobilePromo.${os}`);
-      link.className = `${os}-link text-link`;
-      link.setAttribute("href", "#");
-      return link.outerHTML;
-    });
-    // Put it all together...
-    let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo.text2", formatArgs);
-    // eslint-disable-next-line no-unsanitized/property
-    this.container.querySelector(".device-promo").innerHTML = contents;
-  },
-
   destroy() {
     this._tabListComponent.uninit();
     this.container.remove();
   },
 
   update(state) {
     // Note that we may also want to update elements that are outside of the
     // deck, so use the document to find the class names rather than our
@@ -101,17 +78,16 @@ SyncedTabsDeckView.prototype = {
 
   _clearChilden() {
     while (this.container.firstChild) {
       this.container.firstChild.remove();
     }
   },
 
   _attachListeners() {
-    this.container.querySelector(".android-link").addEventListener("click", this.props.onAndroidClick);
-    this.container.querySelector(".ios-link").addEventListener("click", this.props.oniOSClick);
     let syncPrefLinks = this.container.querySelectorAll(".sync-prefs");
     for (let link of syncPrefLinks) {
       link.addEventListener("click", this.props.onSyncPrefClick);
     }
+    this.container.querySelector(".connect-device").addEventListener("click", this.props.onConnectDeviceClick);
   },
 };
 
--- a/browser/components/syncedtabs/sidebar.xhtml
+++ b/browser/components/syncedtabs/sidebar.xhtml
@@ -78,17 +78,17 @@
         <div class="notAuthedInfo sync-state">
           <div class="syncIllustration"></div>
           <p class="instructions">&syncedTabs.sidebar.notsignedin.label;</p>
           <button class="button sync-prefs">&fxaSignIn.label;</button>
         </div>
         <div class="singleDeviceInfo sync-state">
           <div class="syncIllustrationIssue"></div>
           <p class="instructions">&syncedTabs.sidebar.noclients.subtitle;</p>
-          <p class="instructions device-promo" fxAccountsBrand="&syncBrand.fxAccount.label;"></p>
+          <button class="button connect-device">&syncedTabs.sidebar.connectAnotherDevice;</button>
         </div>
         <div class="tabs-disabled sync-state">
           <div class="syncIllustrationIssue"></div>
           <p class="instructions">&syncedTabs.sidebar.tabsnotsyncing.label;</p>
           <button class="button sync-prefs">&syncedTabs.sidebar.openprefs.label;</button>
         </div>
       </div>
     </template>
--- a/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
+++ b/browser/components/syncedtabs/test/xpcshell/test_SyncedTabsDeckComponent.js
@@ -36,20 +36,18 @@ add_task(async function testInitUninit()
   component.init();
 
   Assert.ok(SyncedTabs.syncTabs.called);
   SyncedTabs.syncTabs.restore();
 
   Assert.ok(ViewMock.calledWithNew(), "view is instantiated");
   Assert.equal(ViewMock.args[0][0], mockWindow);
   Assert.equal(ViewMock.args[0][1], listComponent);
-  Assert.ok(ViewMock.args[0][2].onAndroidClick,
-    "view is passed onAndroidClick prop");
-  Assert.ok(ViewMock.args[0][2].oniOSClick,
-    "view is passed oniOSClick prop");
+  Assert.ok(ViewMock.args[0][2].onConnectDeviceClick,
+    "view is passed onConnectDeviceClick prop");
   Assert.ok(ViewMock.args[0][2].onSyncPrefClick,
     "view is passed onSyncPrefClick prop");
 
   Assert.equal(component.container, view.container,
     "component returns view's container");
 
   Assert.ok(deckStore.on.calledOnce, "listener is added to store");
   Assert.equal(deckStore.on.args[0][0], "change");
@@ -198,39 +196,33 @@ add_task(async function testPanelStatus(
 
   sinon.stub(component, "getPanelStatus", () => Promise.resolve("mock-panelId"));
   sinon.spy(deckStore, "selectPanel");
   await component.updatePanel();
   Assert.ok(deckStore.selectPanel.calledWith("mock-panelId"));
 });
 
 add_task(async function testActions() {
-  let windowMock = {
-    openUILink() {},
-  };
+  let windowMock = {};
   let chromeWindowMock = {
     gSync: {
-      openPrefs() {}
+      openPrefs() {},
+      openConnectAnotherDevice() {}
     }
   };
-  sinon.spy(windowMock, "openUILink");
   sinon.spy(chromeWindowMock.gSync, "openPrefs");
+  sinon.spy(chromeWindowMock.gSync, "openConnectAnotherDevice");
 
   let getChromeWindowMock = sinon.stub();
   getChromeWindowMock.returns(chromeWindowMock);
 
   let component = new SyncedTabsDeckComponent({
     window: windowMock,
     getChromeWindowMock
   });
 
-  let href = Services.prefs.getCharPref("identity.mobilepromo.android") + "synced-tabs-sidebar";
-  component.openAndroidLink("mock-event");
-  Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
-
-  href = Services.prefs.getCharPref("identity.mobilepromo.ios") + "synced-tabs-sidebar";
-  component.openiOSLink("mock-event");
-  Assert.ok(windowMock.openUILink.calledWith(href, "mock-event"));
+  component.openConnectDevice();
+  Assert.ok(chromeWindowMock.gSync.openConnectAnotherDevice.called);
 
   component.openSyncPrefs();
   Assert.ok(getChromeWindowMock.calledWith(windowMock));
   Assert.ok(chromeWindowMock.gSync.openPrefs.called);
 });
--- a/browser/locales/en-US/chrome/browser/accounts.properties
+++ b/browser/locales/en-US/chrome/browser/accounts.properties
@@ -45,21 +45,25 @@ sendToAllDevices.menuitem = Send to All 
 sendTabToDevice.unconfigured.status = Not Connected to Sync
 sendTabToDevice.unconfigured = Learn About Sending Tabs…
 
 # LOCALIZATION NOTE (sendTabToDevice.signintosync)
 # Displayed in the Send Tabs context menu and the page action panel when sync is not
 # configured. Allows users to immediately sign into sync via the preferences.
 sendTabToDevice.signintosync = Sign in to Sync…
 
-# LOCALIZATION NOTE (sendTabToDevice.singledevice, sendTabToDevice.singledevice.status)
+# LOCALIZATION NOTE (sendTabToDevice.singledevice, sendTabToDevice.connectdevice,
+# sendTabToDevice.singledevice.status)
 # Displayed in the Send Tabs context menu when right clicking a tab, a page or a link
-# and the Sync account has only 1 device. Redirects to a marketing page.
+# and the Sync account has only 1 device. The sendTabToDevice.singledevice link
+# redirects to a marketing page, the sendTabToDevice.connectdevice redirects
+# to an FxAccounts page that tells to you to connect another device.
 sendTabToDevice.singledevice.status = No Devices Connected
 sendTabToDevice.singledevice = Learn About Sending Tabs…
+sendTabToDevice.connectdevice = Connect Another Device…
 
 # LOCALIZATION NOTE (sendTabToDevice.verify, sendTabToDevice.verify.status)
 # Displayed in the Send Tabs context menu when right clicking a tab, a page or a link
 # and the Sync account is unverified. Redirects to the Sync preferences page.
 sendTabToDevice.verify.status = Account Not Verified
 sendTabToDevice.verify = Verify Your Account…
 
 # LOCALIZATION NOTE (tabArrivingNotification.title, tabArrivingNotificationWithDevice.title,
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -379,16 +379,17 @@ These should match what Safari and other
      when Sync is configured but this appears to be the only device attached to
      the account. We also show links to download Firefox for android/ios. -->
 <!ENTITY appMenuRemoteTabs.noclients.subtitle "Want to see your tabs from other devices here?">
 <!ENTITY appMenuRemoteTabs.openprefs.label "Sync Preferences">
 <!ENTITY appMenuRemoteTabs.notsignedin.label "Sign in to view a list of tabs from your other devices.">
 <!ENTITY appMenuRemoteTabs.signin.label "Sign in to Sync">
 <!ENTITY appMenuRemoteTabs.managedevices.label "Manage Devices…">
 <!ENTITY appMenuRemoteTabs.sidebar.label "View Synced Tabs Sidebar">
+<!ENTITY appMenuRemoteTabs.connectdevice.label "Connect Another Device">
 
 <!ENTITY appMenuRecentHighlights.label "Recent Highlights">
 
 <!ENTITY customizeMenu.addToToolbar.label "Add to Toolbar">
 <!ENTITY customizeMenu.addToToolbar.accesskey "A">
 <!ENTITY customizeMenu.addToPanel.label "Add to Menu">
 <!ENTITY customizeMenu.addToPanel.accesskey "M">
 <!-- LOCALIZATION NOTE (customizeMenu.addToOverflowMenu.label,
@@ -794,16 +795,17 @@ you can use these alternative items. Oth
 <!ENTITY syncedTabs.sidebar.noclients.subtitle "Want to see your tabs from other devices here?">
 <!ENTITY syncedTabs.sidebar.notsignedin.label  "Sign in to view a list of tabs from your other devices.">
 <!ENTITY syncedTabs.sidebar.notabs.label       "No open tabs">
 <!ENTITY syncedTabs.sidebar.openprefs.label    "Open &syncBrand.shortName.label; Preferences">
 <!-- LOCALIZATION NOTE (syncedTabs.sidebar.tabsnotsyncing.label): This is shown
      when Sync is configured but syncing tabs is disabled. -->
 <!ENTITY syncedTabs.sidebar.tabsnotsyncing.label       "Turn on tab syncing to view a list of tabs from your other devices.">
 <!ENTITY syncedTabs.sidebar.searchPlaceholder  "Search synced tabs">
+<!ENTITY syncedTabs.sidebar.connectAnotherDevice  "Connect Another Device">
 
 <!-- LOCALIZATION NOTE (syncedTabs.context.open.accesskey,
                         syncedTabs.context.openAllInTabs.accesskey):
      These access keys are identical because their associated menu items are
      mutually exclusive -->
 <!ENTITY syncedTabs.context.open.label                       "Open">
 <!ENTITY syncedTabs.context.open.accesskey                   "O">
 <!ENTITY syncedTabs.context.openInNewTab.label               "Open in a New Tab">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -806,27 +806,16 @@ slowStartup.disableNotificationButton.ac
 # LOCALIZATION NOTE  - %S is brandShortName
 flashHang.message = %S changed some Adobe Flash settings to improve performance.
 flashHang.helpButton.label = Learn More…
 flashHang.helpButton.accesskey = L
 
 # LOCALIZATION NOTE (customizeMode.tabTitle): %S is brandShortName
 customizeMode.tabTitle = Customize %S
 
-# LOCALIZATION NOTE (appMenuRemoteTabs.mobilePromo.text2):
-# %1$S will be replaced with a link, the text of which is
-# appMenuRemoteTabs.mobilePromo.android and the link will be to
-# https://www.mozilla.org/firefox/android/.
-# %2$S will be replaced with a link, the text of which is
-# appMenuRemoteTabs.mobilePromo.ios
-# and the link will be to https://www.mozilla.org/firefox/ios/.
-appMenuRemoteTabs.mobilePromo.text2 = Download %1$S or %2$S and connect them to your Firefox Account.
-appMenuRemoteTabs.mobilePromo.android = Firefox for Android
-appMenuRemoteTabs.mobilePromo.ios = Firefox for iOS
-
 # LOCALIZATION NOTE (e10s.accessibilityNotice.mainMessage,
 #                    e10s.accessibilityNotice.enableAndRestart.label,
 #                    e10s.accessibilityNotice.enableAndRestart.accesskey):
 # These strings are related to the messages we display to offer e10s (Multi-process) to users
 # on the pre-release channels. They won't be used in release but they will likely be used in
 # beta starting from version 41, so it's still useful to have these strings properly localized.
 # %S is brandShortName
 e10s.accessibilityNotice.mainMessage2 = Accessibility support is partially disabled due to compatibility issues with new %S features.
@@ -921,9 +910,9 @@ permissions.remove.tooltip = Clear this 
 # between the Firefox version and the "What's new" link in the About dialog,
 # e.g.: "48.0.2 (32-bit) <What's new>" or "51.0a1 (2016-09-05) (64-bit)".
 aboutDialog.architecture.sixtyFourBit = 64-bit
 aboutDialog.architecture.thirtyTwoBit = 32-bit
 
 # LOCALIZATION NOTE (certImminentDistrust.message):
 # Shown in the browser console when visiting a website that is trusted today,
 # but won't be in the future unless the site operator makes a change.
-certImminentDistrust.message = The security certificate in use on this website will no longer be trusted in a future release. For more information, visit https://wiki.mozilla.org/CA/Upcoming_Distrust_Actions
\ No newline at end of file
+certImminentDistrust.message = The security certificate in use on this website will no longer be trusted in a future release. For more information, visit https://wiki.mozilla.org/CA/Upcoming_Distrust_Actions
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -617,18 +617,17 @@ toolbarbutton[constrain-size="true"][cui
   border-inline-start-style: none;
 }
 
 #PanelUI-remotetabs {
   --panel-ui-sync-illustration-height: 157.5px;
 }
 
 .PanelUI-remotetabs-instruction-title,
-.PanelUI-remotetabs-instruction-label,
-#PanelUI-remotetabs-mobile-promo {
+.PanelUI-remotetabs-instruction-label {
   /* If you change the margin here, the min-height of the synced tabs panel
     (e.g. #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync, etc) may
     need adjusting (see bug 1248506) */
   margin: 15px;
   text-align: center;
   text-shadow: none;
   max-width: 15em;
   color: GrayText;
@@ -643,38 +642,38 @@ toolbarbutton[constrain-size="true"][cui
 .PanelUI-remotetabs-instruction-box {
   /* If you change the padding here, the min-height of the synced tabs panel
     (e.g. #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync, etc) may
     need adjusting (see bug 1248506) */
   padding-bottom: 30px;
   padding-top: 15px;
 }
 
-.PanelUI-remotetabs-prefs-button {
+.PanelUI-remotetabs-button {
   -moz-appearance: none;
   background-color: #0060df;
   /* !important for the color as an OSX specific rule when a lightweight theme
      is used for buttons in the toolbox overrides. See bug 1238531 for details */
   color: white !important;
   border-radius: 2px;
   /* If you change the margin or padding below, the min-height of the synced tabs
      panel (e.g. #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync,
      etc) may need adjusting (see bug 1248506) */
   margin-top: 10px;
   margin-bottom: 10px;
   padding: 8px;
   text-shadow: none;
   min-width: 200px;
 }
 
-.PanelUI-remotetabs-prefs-button:hover {
+.PanelUI-remotetabs-button:hover {
   background-color: #003eaa;
 }
 
-.PanelUI-remotetabs-prefs-button:hover:active {
+.PanelUI-remotetabs-button:hover:active {
   background-color: #002275;
 }
 
 .remotetabs-promo-link {
   margin: 0;
 }
 
 .PanelUI-remotetabs-notabsforclient-label {
@@ -698,17 +697,17 @@ toolbarbutton[constrain-size="true"][cui
 .fxaSyncIllustration {
   list-style-image: url(chrome://browser/skin/fxa/sync-illustration.svg);
 }
 
 .fxaSyncIllustrationIssue {
   list-style-image: url(chrome://browser/skin/fxa/sync-illustration-issue.svg);
 }
 
-.PanelUI-remotetabs-prefs-button > .toolbarbutton-text {
+.PanelUI-remotetabs-button > .toolbarbutton-text {
   /* !important to override ".cui-widget-panel toolbarbutton > .toolbarbutton-text" above. */
   text-align: center !important;
   text-shadow: none;
 }
 
 #PanelUI-remotetabs[mainview] { /* panel anchored to toolbar button might be too skinny */
   min-width: 19em;
 }
@@ -716,18 +715,18 @@ toolbarbutton[constrain-size="true"][cui
 /* Work around bug 1224412 - these boxes will cause scrollbars to appear when
    the panel is anchored to a toolbar button.
 */
 #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-setupsync,
 #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-reauthsync,
 #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-nodevicespane,
 #PanelUI-remotetabs[mainview] #PanelUI-remotetabs-tabsdisabledpane {
   min-height: calc(var(--panel-ui-sync-illustration-height) +
-                   20px + /* margin of .PanelUI-remotetabs-prefs-button */
-                   16px + /* padding of .PanelUI-remotetabs-prefs-button */
+                   20px + /* margin of .PanelUI-remotetabs-button */
+                   16px + /* padding of .PanelUI-remotetabs-button */
                    30px + /* margin of .PanelUI-remotetabs-instruction-label */
                    30px + 15px + /* padding of .PanelUI-remotetabs-instruction-box */
                    11em);
 }
 
 #PanelUI-remotetabs-tabslist > label[itemtype="client"] {
   color: GrayText;
 }
--- a/services/fxaccounts/FxAccountsConfig.jsm
+++ b/services/fxaccounts/FxAccountsConfig.jsm
@@ -23,16 +23,17 @@ const CONFIG_PREFS = [
   "identity.fxaccounts.remote.profile.uri",
   "identity.sync.tokenserver.uri",
   "identity.fxaccounts.remote.webchannel.uri",
   "identity.fxaccounts.settings.uri",
   "identity.fxaccounts.settings.devices.uri",
   "identity.fxaccounts.remote.signup.uri",
   "identity.fxaccounts.remote.signin.uri",
   "identity.fxaccounts.remote.email.uri",
+  "identity.fxaccounts.remote.connectdevice.uri",
   "identity.fxaccounts.remote.force_auth.uri",
 ];
 
 this.FxAccountsConfig = {
 
   async _getPrefURL(prefName) {
     await this.ensureConfigured();
     let url = Services.urlFormatter.formatURLPref(prefName);
@@ -140,16 +141,17 @@ this.FxAccountsConfig = {
         Services.prefs.getCharPref("identity.fxaccounts.contextParam"));
 
       Services.prefs.setCharPref("identity.fxaccounts.remote.webchannel.uri", rootURL);
       Services.prefs.setCharPref("identity.fxaccounts.settings.uri", rootURL + "/settings?service=sync&context=" + contextParam);
       Services.prefs.setCharPref("identity.fxaccounts.settings.devices.uri", rootURL + "/settings/clients?service=sync&context=" + contextParam);
       Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", rootURL + "/signup?service=sync&context=" + contextParam);
       Services.prefs.setCharPref("identity.fxaccounts.remote.signin.uri", rootURL + "/signin?service=sync&context=" + contextParam);
       Services.prefs.setCharPref("identity.fxaccounts.remote.email.uri", rootURL + "/?service=sync&context=" + contextParam + "&action=email");
+      Services.prefs.setCharPref("identity.fxaccounts.remote.connectdevice.uri", rootURL + "/connect_another_device?service=sync&context=" + contextParam);
       Services.prefs.setCharPref("identity.fxaccounts.remote.force_auth.uri", rootURL + "/force_auth?service=sync&context=" + contextParam);
 
       // Ensure the webchannel is pointed at the correct uri
       EnsureFxAccountsWebChannel();
     } catch (e) {
       log.error("Failed to initialize configuration preferences from autoconfig object", e);
       throw e;
     }