1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 // This file is loaded into the browser window scope.
6 /* eslint-env mozilla/browser-window */
9 FX_MONITOR_OAUTH_CLIENT_ID,
10 FX_RELAY_OAUTH_CLIENT_ID,
12 } = ChromeUtils.importESModule(
13 "resource://gre/modules/FxAccountsCommon.sys.mjs"
16 const { UIState } = ChromeUtils.importESModule(
17 "resource://services-sync/UIState.sys.mjs"
20 ChromeUtils.defineESModuleGetters(this, {
21 EnsureFxAccountsWebChannel:
22 "resource://gre/modules/FxAccountsWebChannel.sys.mjs",
24 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs",
25 FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs",
26 SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
27 Weave: "resource://services-sync/main.sys.mjs",
30 const MIN_STATUS_ANIMATION_DURATION = 1600;
32 this.SyncedTabsPanelList = class SyncedTabsPanelList {
33 static sRemoteTabsDeckIndices = {
35 DECKINDEX_FETCHING: 1,
36 DECKINDEX_TABSDISABLED: 2,
37 DECKINDEX_NOCLIENTS: 3,
40 static sRemoteTabsPerPage = 25;
41 static sRemoteTabsNextPageMinTabs = 5;
43 constructor(panelview, deck, tabsList, separator) {
44 this.QueryInterface = ChromeUtils.generateQI([
46 "nsISupportsWeakReference",
49 Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED, true);
51 this.tabsList = tabsList;
52 this.separator = separator;
53 this._showSyncedTabsPromise = Promise.resolve();
55 this.createSyncedTabs();
58 observe(subject, topic) {
59 if (topic == SyncedTabs.TOPIC_TABS_CHANGED) {
60 this._showSyncedTabs();
65 if (SyncedTabs.isConfiguredToSyncTabs) {
66 if (SyncedTabs.hasSyncedThisSession) {
67 this.deck.selectedIndex =
68 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABS;
70 // Sync hasn't synced tabs yet, so show the "fetching" panel.
71 this.deck.selectedIndex =
72 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_FETCHING;
74 // force a background sync.
75 SyncedTabs.syncTabs().catch(ex => {
78 this.deck.toggleAttribute("syncingtabs", true);
79 // show the current list - it will be updated by our observer.
80 this._showSyncedTabs();
82 this.separator.hidden = false;
85 // not configured to sync tabs, so no point updating the list.
86 this.deck.selectedIndex =
87 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABSDISABLED;
88 this.deck.toggleAttribute("syncingtabs", false);
90 this.separator.hidden = true;
95 // Update the synced tab list after any existing in-flight updates are complete.
96 _showSyncedTabs(paginationInfo) {
97 this._showSyncedTabsPromise = this._showSyncedTabsPromise.then(
99 return this.__showSyncedTabs(paginationInfo);
107 // Return a new promise to update the tab list.
108 __showSyncedTabs(paginationInfo) {
109 if (!this.tabsList) {
110 // Closed between the previous `this._showSyncedTabsPromise`
111 // resolving and now.
114 return SyncedTabs.getTabClients()
116 let noTabs = !UIState.get().syncEnabled || !clients.length;
117 this.deck.toggleAttribute("syncingtabs", !noTabs);
118 if (this.separator) {
119 this.separator.hidden = noTabs;
122 // The view may have been hidden while the promise was resolving.
123 if (!this.tabsList) {
126 if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) {
127 // the "fetching tabs" deck is being shown - let's leave it there.
128 // When that first sync completes we'll be notified and update.
132 if (clients.length === 0) {
133 this.deck.selectedIndex =
134 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_NOCLIENTS;
137 this.deck.selectedIndex =
138 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABS;
139 this._clearSyncedTabList();
140 SyncedTabs.sortTabClientsByLastUsed(clients);
141 let fragment = document.createDocumentFragment();
143 let clientNumber = 0;
144 for (let client of clients) {
145 // add a menu separator for all clients other than the first.
146 if (fragment.lastElementChild) {
147 let separator = document.createXULElement("toolbarseparator");
148 fragment.appendChild(separator);
150 // We add the client's elements to a container, and indicate which
151 // element labels it.
152 let labelId = `synced-tabs-client-${clientNumber++}`;
153 let container = document.createXULElement("vbox");
154 container.classList.add("PanelUI-remotetabs-clientcontainer");
155 container.setAttribute("role", "group");
156 container.setAttribute("aria-labelledby", labelId);
157 let clientPaginationInfo =
158 paginationInfo && paginationInfo.clientId == client.id
160 : { clientId: client.id };
161 this._appendSyncClient(
167 fragment.appendChild(container);
169 this.tabsList.appendChild(fragment);
175 // an observer for tests.
176 Services.obs.notifyObservers(
178 "synced-tabs-menu:test:tabs-updated"
183 _clearSyncedTabList() {
184 let list = this.tabsList;
185 while (list.lastChild) {
186 list.lastChild.remove();
190 _createNoSyncedTabsElement(messageAttr, appendTo = null) {
192 appendTo = this.tabsList;
195 let messageLabel = document.createXULElement("label");
196 document.l10n.setAttributes(
198 this.tabsList.getAttribute(messageAttr)
200 appendTo.appendChild(messageLabel);
204 _appendSyncClient(client, container, labelId, paginationInfo) {
206 maxTabs = SyncedTabsPanelList.sRemoteTabsPerPage,
207 showInactive = false,
209 // Create the element for the remote client.
210 let clientItem = document.createXULElement("label");
211 clientItem.setAttribute("id", labelId);
212 clientItem.setAttribute("itemtype", "client");
213 clientItem.setAttribute(
215 gSync.fluentStrings.formatValueSync("appmenu-fxa-last-sync", {
216 time: gSync.formatLastSyncDate(new Date(client.lastModified)),
219 clientItem.textContent = client.name;
221 container.appendChild(clientItem);
223 if (!client.tabs.length) {
224 let label = this._createNoSyncedTabsElement(
225 "notabsforclientlabel",
228 label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
230 let tabs = client.tabs.filter(t => showInactive || !t.inactive);
231 let numInactive = client.tabs.length - tabs.length;
233 // If this page will display all tabs, show no additional buttons.
234 // Otherwise, show a "Show More" button
235 let hasNextPage = tabs.length > maxTabs;
236 let nextPageIsLastPage =
238 maxTabs + SyncedTabsPanelList.sRemoteTabsPerPage >= tabs.length;
239 if (nextPageIsLastPage) {
240 // When the user clicks "Show More", try to have at least sRemoteTabsNextPageMinTabs more tabs
241 // to display in order to avoid user frustration
243 tabs.length - SyncedTabsPanelList.sRemoteTabsNextPageMinTabs,
248 tabs = tabs.slice(0, maxTabs);
250 for (let [index, tab] of tabs.entries()) {
251 let tabEnt = this._createSyncedTabElement(tab, index);
252 container.appendChild(tabEnt);
255 let elt = this._createShowInactiveTabsElement(
259 container.appendChild(elt);
262 let showAllEnt = this._createShowMoreSyncedTabsElement(paginationInfo);
263 container.appendChild(showAllEnt);
268 _createSyncedTabElement(tabInfo, index) {
269 let item = document.createXULElement("toolbarbutton");
270 let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
271 item.setAttribute("itemtype", "tab");
272 item.setAttribute("class", "subviewbutton");
273 item.setAttribute("targetURI", tabInfo.url);
276 tabInfo.title != "" ? tabInfo.title : tabInfo.url
279 item.setAttribute("image", tabInfo.icon);
281 item.setAttribute("tooltiptext", tooltipText);
282 // We need to use "click" instead of "command" here so openUILink
283 // respects different buttons (eg, to open in a new tab).
284 item.addEventListener("click", e => {
285 // We want to differentiate between when the fxa panel is within the app menu/hamburger bar
286 let object = "fxa_avatar_menu";
287 const appMenuPanel = document.getElementById("appMenu-popup");
288 if (appMenuPanel.contains(e.currentTarget)) {
289 object = "fxa_app_menu";
291 SyncedTabs.recordSyncedTabsTelemetry(object, "click", {
292 tab_pos: index.toString(),
294 document.defaultView.openUILink(tabInfo.url, e, {
295 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
299 if (document.defaultView.whereToOpenLink(e) != "current") {
303 CustomizableUI.hidePanelForNode(item);
309 _createShowMoreSyncedTabsElement(paginationInfo) {
310 let showMoreItem = document.createXULElement("toolbarbutton");
311 showMoreItem.setAttribute("itemtype", "showmorebutton");
312 showMoreItem.setAttribute("closemenu", "none");
313 showMoreItem.classList.add(
316 "subviewbutton-nav-down"
318 document.l10n.setAttributes(showMoreItem, "appmenu-remote-tabs-showmore");
320 paginationInfo.maxTabs = Infinity;
321 showMoreItem.addEventListener("click", e => {
324 this._showSyncedTabs(paginationInfo);
329 _createShowInactiveTabsElement(paginationInfo, count) {
330 let showItem = document.createXULElement("toolbarbutton");
331 showItem.setAttribute("itemtype", "showmorebutton");
332 showItem.setAttribute("closemenu", "none");
333 showItem.classList.add(
336 "subviewbutton-nav-down"
338 document.l10n.setAttributes(showItem, "appmenu-remote-tabs-showinactive");
339 document.l10n.setArgs(showItem, { count });
341 paginationInfo.showInactive = true;
342 showItem.addEventListener("click", e => {
345 this._showSyncedTabs(paginationInfo);
351 Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
352 this.tabsList = null;
354 this.separator = null;
360 _isCurrentlySyncing: false,
361 // The last sync start time. Used to calculate the leftover animation time
362 // once syncing completes (bug 1239042).
364 _syncAnimationTimer: 0,
365 _obs: ["weave:engine:sync:finish", "quit-application", UIState.ON_UPDATE],
369 const { Log } = ChromeUtils.importESModule(
370 "resource://gre/modules/Log.sys.mjs"
372 let syncLog = Log.repository.getLogger("Sync.Browser");
373 syncLog.manageLevelFromPref("services.sync.log.logger.browser");
379 get fluentStrings() {
380 delete this.fluentStrings;
381 return (this.fluentStrings = new Localization(
383 "branding/brand.ftl",
384 "browser/accounts.ftl",
385 "browser/appmenu.ftl",
387 "toolkit/branding/accounts.ftl",
393 // Returns true if FxA is configured, but the send tab targets list isn't
395 get sendTabConfiguredAndLoading() {
397 UIState.get().status == UIState.STATUS_SIGNED_IN &&
398 !fxAccounts.device.recentDeviceList
403 return UIState.get().status == UIState.STATUS_SIGNED_IN;
406 shouldHideSendContextMenuItems(enabled) {
407 const state = UIState.get();
408 // Only show the "Send..." context menu items when sending would be possible
411 state.status == UIState.STATUS_SIGNED_IN &&
413 this.getSendTabTargets().length
420 getSendTabTargets() {
423 UIState.get().status != UIState.STATUS_SIGNED_IN ||
424 !fxAccounts.device.recentDeviceList
428 for (let d of fxAccounts.device.recentDeviceList) {
429 if (d.isCurrentDevice) {
433 if (fxAccounts.commands.sendTab.isDeviceCompatible(d)) {
437 return targets.sort((a, b) => b.lastAccessTime - a.lastAccessTime);
440 _definePrefGetters() {
441 XPCOMUtils.defineLazyPreferenceGetter(
444 "identity.fxaccounts.enabled"
446 XPCOMUtils.defineLazyPreferenceGetter(
448 "PXI_TOOLBAR_ENABLED",
449 "identity.fxaccounts.toolbar.pxiToolbarEnabled"
453 maybeUpdateUIState() {
455 if (UIState.isReady()) {
456 const state = UIState.get();
457 // If we are not configured, the UI is already in the right state when
458 // we open the window. We can avoid a repaint.
459 if (state.status != UIState.STATUS_NOT_CONFIGURED) {
460 this.updateAllUI(state);
466 if (this._initialized) {
470 this._definePrefGetters();
472 if (!this.FXA_ENABLED) {
473 this.onFxaDisabled();
477 MozXULElement.insertFTLIfNeeded("browser/sync.ftl");
479 // Label for the sync buttons.
480 const appMenuLabel = PanelMultiView.getViewNode(
485 // We are in a window without our elements - just abort now, without
486 // setting this._initialized, so we don't attempt to remove observers.
489 // We start with every menuitem hidden (except for the "setup sync" state),
490 // so that we don't need to init the sync UI on windows like pageInfo.xhtml
491 // (see bug 1384856).
492 // maybeUpdateUIState() also optimizes for this - if we should be in the
493 // "setup sync" state, that function assumes we are already in it and
494 // doesn't re-initialize the UI elements.
495 document.getElementById("sync-setup").hidden = false;
496 PanelMultiView.getViewNode(
498 "PanelUI-remotetabs-setupsync"
501 const appMenuHeaderTitle = PanelMultiView.getViewNode(
503 "appMenu-header-title"
505 const appMenuHeaderDescription = PanelMultiView.getViewNode(
507 "appMenu-header-description"
509 const appMenuHeaderText = PanelMultiView.getViewNode(
513 appMenuHeaderTitle.hidden = true;
514 // We must initialize the label attribute here instead of the markup
515 // due to a timing error. The fluent label attribute was being applied
516 // after we had updated appMenuLabel and thus displayed an incorrect
517 // label for signed in users.
518 const [headerDesc, headerText] = this.fluentStrings.formatValuesSync([
519 "appmenu-fxa-signed-in-label",
520 "appmenu-fxa-sync-and-save-data2",
522 appMenuHeaderDescription.value = headerDesc;
523 appMenuHeaderText.textContent = headerText;
525 for (let topic of this._obs) {
526 Services.obs.addObserver(this, topic, true);
529 this.maybeUpdateUIState();
531 EnsureFxAccountsWebChannel();
533 let fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa");
534 fxaPanelView.addEventListener("ViewShowing", this);
535 fxaPanelView.addEventListener("ViewHiding", this);
537 // If the experiment is enabled, we'll need to update the panels
538 // to show some different text to the user
539 if (this.PXI_TOOLBAR_ENABLED) {
540 this.updateFxAPanel(UIState.get());
541 this.updateCTAPanel();
544 this._initialized = true;
548 if (!this._initialized) {
552 for (let topic of this._obs) {
553 Services.obs.removeObserver(this, topic);
556 this._initialized = false;
560 switch (event.type) {
561 case "ViewShowing": {
562 this.onFxAPanelViewShowing(event.target);
566 this.onFxAPanelViewHiding(event.target);
571 onFxAPanelViewShowing(panelview) {
572 let syncNowBtn = panelview.querySelector(".syncnow-label");
573 let l10nId = syncNowBtn.getAttribute(
574 this._isCurrentlySyncing
575 ? "syncing-data-l10n-id"
576 : "sync-now-data-l10n-id"
578 document.l10n.setAttributes(syncNowBtn, l10nId);
580 // This needs to exist because if the user is signed in
581 // but the user disabled or disconnected sync we should not show the button
582 const syncPrefsButtonEl = PanelMultiView.getViewNode(
584 "PanelUI-fxa-menu-sync-prefs-button"
586 syncPrefsButtonEl.hidden = !UIState.get().syncEnabled;
588 // We should ensure that we do not show the sign out button
589 // if the user is not signed in
590 const signOutButtonEl = PanelMultiView.getViewNode(
592 "PanelUI-fxa-menu-account-signout-button"
594 signOutButtonEl.hidden = !this.isSignedIn;
596 panelview.syncedTabsPanelList = new SyncedTabsPanelList(
598 PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-deck"),
599 PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-tabslist"),
600 PanelMultiView.getViewNode(document, "PanelUI-remote-tabs-separator")
604 onFxAPanelViewHiding(panelview) {
605 panelview.syncedTabsPanelList.destroy();
606 panelview.syncedTabsPanelList = null;
609 observe(subject, topic, data) {
610 if (!this._initialized) {
611 console.error("browser-sync observer called after unload: ", topic);
615 case UIState.ON_UPDATE:
616 const state = UIState.get();
617 this.updateAllUI(state);
619 case "quit-application":
620 // Stop the animation timer on shutdown, since we can't update the UI
622 clearTimeout(this._syncAnimationTimer);
624 case "weave:engine:sync:finish":
625 if (data != "clients") {
628 this.onClientsSynced();
629 this.updateFxAPanel(UIState.get());
635 this.updatePanelPopup(state);
636 this.updateState(state);
637 this.updateSyncButtonsTooltip(state);
638 this.updateSyncStatus(state);
639 this.updateFxAPanel(state);
640 this.updateCTAPanel(state);
641 // Ensure we have something in the device list in the background.
642 this.ensureFxaDevices();
645 // Ensure we have *something* in `fxAccounts.device.recentDeviceList` as some
646 // of our UI logic depends on it not being null. When FxA is notified of a
647 // device change it will auto refresh `recentDeviceList`, and all UI which
648 // shows the device list will start with `recentDeviceList`, but should also
649 // force a refresh, both of which should mean in the worst-case, the UI is up
650 // to date after a very short delay.
651 async ensureFxaDevices() {
652 if (UIState.get().status != UIState.STATUS_SIGNED_IN) {
653 console.info("Skipping device list refresh; not signed in");
656 if (!fxAccounts.device.recentDeviceList) {
657 if (await this.refreshFxaDevices()) {
658 // Assuming we made the call successfully it should be impossible to end
659 // up with a falsey recentDeviceList, so make noise if that's false.
660 if (!fxAccounts.device.recentDeviceList) {
661 console.warn("Refreshing device list didn't find any devices.");
667 // Force a refresh of the fxa device list. Note that while it's theoretically
668 // OK to call `fxAccounts.device.refreshDeviceList` multiple times concurrently
669 // and regularly, this call tells it to avoid those protections, so will always
670 // hit the FxA servers - therefore, you should be very careful how often you
672 // Returns Promise<bool> to indicate whether a refresh was actually done.
673 async refreshFxaDevices() {
674 if (UIState.get().status != UIState.STATUS_SIGNED_IN) {
675 console.info("Skipping device list refresh; not signed in");
679 // Do the actual refresh telling it to avoid the "flooding" protections.
680 await fxAccounts.device.refreshDeviceList({ ignoreCached: true });
683 this.log.error("Refreshing device list failed.", e);
688 updateSendToDeviceTitle() {
689 const tabCount = gBrowser.selectedTab.multiselected
690 ? gBrowser.selectedTabs.length
692 document.l10n.setArgs(
693 PanelMultiView.getViewNode(document, "PanelUI-fxa-menu-sendtab-button"),
698 showSendToDeviceView(anchor) {
699 PanelUI.showSubView("PanelUI-sendTabToDevice", anchor);
700 let panelViewNode = document.getElementById("PanelUI-sendTabToDevice");
701 this._populateSendTabToDevicesView(panelViewNode);
704 showSendToDeviceViewFromFxaMenu(anchor) {
705 const { status } = UIState.get();
706 if (status === UIState.STATUS_NOT_CONFIGURED) {
707 PanelUI.showSubView("PanelUI-fxa-menu-sendtab-not-configured", anchor);
711 const targets = this.sendTabConfiguredAndLoading
713 : this.getSendTabTargets();
714 if (!targets.length) {
715 PanelUI.showSubView("PanelUI-fxa-menu-sendtab-no-devices", anchor);
719 this.showSendToDeviceView(anchor);
720 this.emitFxaToolbarTelemetry("send_tab", anchor);
723 showRemoteTabsFromFxaMenu(panel) {
724 PanelUI.showSubView("PanelUI-remotetabs", panel);
725 this.emitFxaToolbarTelemetry("sync_tabs", panel);
728 showSidebarFromFxaMenu(panel) {
729 SidebarUI.toggle("viewTabsSidebar");
730 this.emitFxaToolbarTelemetry("sync_tabs_sidebar", panel);
733 _populateSendTabToDevicesView(panelViewNode, reloadDevices = true) {
734 let bodyNode = panelViewNode.querySelector(".panel-subview-body");
735 let panelNode = panelViewNode.closest("panel");
736 let browser = gBrowser.selectedBrowser;
737 let uri = browser.currentURI;
738 let title = browser.contentTitle;
739 let multiselected = gBrowser.selectedTab.multiselected;
741 // This is on top because it also clears the device list between state
743 this.populateSendTabToDevicesMenu(
748 (clientId, name, clientType, lastModified) => {
750 return document.createXULElement("toolbarseparator");
752 let item = document.createXULElement("toolbarbutton");
753 item.setAttribute("wrap", true);
754 item.setAttribute("align", "start");
755 item.classList.add("sendToDevice-device", "subviewbutton");
757 item.classList.add("subviewbutton-iconic");
759 let lastSyncDate = gSync.formatLastSyncDate(lastModified);
763 this.fluentStrings.formatValueSync("appmenu-fxa-last-sync", {
771 item.addEventListener("command", () => {
773 PanelMultiView.hidePopup(panelNode);
781 bodyNode.removeAttribute("state");
782 // If the app just started, we won't have fetched the device list yet. Sync
783 // does this automatically ~10 sec after startup, but there's no trigger for
784 // this if we're signed in to FxA, but not Sync.
785 if (gSync.sendTabConfiguredAndLoading) {
786 bodyNode.setAttribute("state", "notready");
789 // We will only pick up new Fennec clients if we sync the clients engine,
790 // but all other send-tab targets can be identified purely from the fxa
791 // device list. Syncing the clients engine doesn't force a refresh of the
792 // fxa list, and it seems overkill to force *both* a clients engine sync
793 // and an fxa device list refresh, especially given (a) the clients engine
794 // will sync by itself every 10 minutes and (b) Fennec is (at time of
795 // writing) about to be replaced by Fenix.
796 // So we suck up the fact that new Fennec clients may not appear for 10
797 // minutes and don't bother syncing the clients engine.
799 // Force a refresh of the fxa device list in case the user connected a new
800 // device, and is waiting for it to show up.
801 this.refreshFxaDevices().then(_ => {
802 if (!window.closed) {
803 this._populateSendTabToDevicesView(panelViewNode, false);
810 anchor = document.getElementById("fxa-toolbar-menu-button"),
813 // Don't show the panel if the window is in customization mode.
814 if (document.documentElement.hasAttribute("customizing")) {
819 (aEvent.type == "mousedown" && aEvent.button != 0) ||
820 (aEvent.type == "keypress" &&
821 aEvent.charCode != KeyEvent.DOM_VK_SPACE &&
822 aEvent.keyCode != KeyEvent.DOM_VK_RETURN)
827 // We read the state that's been set on the root node, since that makes
828 // it easier to test the various front-end states without having to actually
829 // have UIState know about it.
830 let fxaStatus = document.documentElement.getAttribute("fxastatus");
832 if (fxaStatus == "not_configured") {
833 // If we're signed out but have the PXI pref enabled
834 // we should show the PXI panel instead of taking the user
835 // straight to FxA sign-in
836 if (this.PXI_TOOLBAR_ENABLED) {
837 this.updateFxAPanel(UIState.get());
838 this.updateCTAPanel();
839 PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
840 } else if (anchor == document.getElementById("fxa-toolbar-menu-button")) {
841 // The fxa toolbar button doesn't have much context before the user
842 // clicks it so instead of going straight to the login page,
843 // we take them to a page that has more information
844 this.emitFxaToolbarTelemetry("toolbar_icon", anchor);
845 openTrustedLinkIn("about:preferences#sync", "tab");
849 anchor.id == "appMenu-fxa-label2"
850 ? PanelMultiView.getViewNode(document, "PanelUI-fxa")
852 this.openFxAEmailFirstPageFromFxaMenu(panel);
857 // If the user is signed in and we have the PXI pref enabled then add
858 // the pxi panel to the existing toolbar
859 if (this.PXI_TOOLBAR_ENABLED) {
860 this.updateCTAPanel();
863 if (!gFxaToolbarAccessed) {
864 Services.prefs.setBoolPref("identity.fxaccounts.toolbar.accessed", true);
867 this.enableSendTabIfValidTab();
869 if (!this.getSendTabTargets().length) {
870 PanelMultiView.getViewNode(
872 "PanelUI-fxa-menu-sendtab-button"
876 if (anchor.getAttribute("open") == "true") {
879 this.emitFxaToolbarTelemetry("toolbar_icon", anchor);
880 PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
884 updateFxAPanel(state = {}) {
885 const mainWindowEl = document.documentElement;
887 // The Firefox Account toolbar currently handles 3 different states for
888 // users. The default `not_configured` state shows an empty avatar, `unverified`
889 // state shows an avatar with an email icon, `login-failed` state shows an avatar
890 // with a danger icon and the `verified` state will show the users
891 // custom profile image or a filled avatar.
892 let stateValue = "not_configured";
894 const menuHeaderTitleEl = PanelMultiView.getViewNode(
896 "fxa-menu-header-title"
898 const menuHeaderDescriptionEl = PanelMultiView.getViewNode(
900 "fxa-menu-header-description"
903 const cadButtonEl = PanelMultiView.getViewNode(
905 "PanelUI-fxa-menu-connect-device-button"
908 const syncSetupButtonEl = PanelMultiView.getViewNode(
910 "PanelUI-fxa-menu-setup-sync-button"
913 const syncNowButtonEl = PanelMultiView.getViewNode(
915 "PanelUI-fxa-menu-syncnow-button"
918 const fxaMenuAccountButtonEl = PanelMultiView.getViewNode(
920 "fxa-manage-account-button"
923 const signedInContainer = PanelMultiView.getViewNode(
925 "PanelUI-signedin-panel"
928 cadButtonEl.setAttribute("disabled", true);
929 syncNowButtonEl.hidden = true;
930 signedInContainer.hidden = true;
931 fxaMenuAccountButtonEl.classList.remove("subviewbutton-nav");
932 fxaMenuAccountButtonEl.removeAttribute("closemenu");
933 syncSetupButtonEl.removeAttribute("hidden");
935 let headerTitleL10nId = this.PXI_TOOLBAR_ENABLED
936 ? "appmenuitem-sign-in-account"
937 : "appmenuitem-fxa-sign-in";
938 let headerDescription;
939 if (state.status === UIState.STATUS_NOT_CONFIGURED) {
940 mainWindowEl.style.removeProperty("--avatar-image-url");
941 headerDescription = this.fluentStrings.formatValueSync(
942 "appmenu-fxa-signed-in-label"
944 // Signed out, expeirment enabled is the only state we want to hide the
945 // header description, so we make it empty and check for that when setting
947 if (this.PXI_TOOLBAR_ENABLED) {
948 headerDescription = "";
950 } else if (state.status === UIState.STATUS_LOGIN_FAILED) {
951 stateValue = "login-failed";
952 headerTitleL10nId = "account-disconnected2";
953 headerDescription = state.displayName || state.email;
954 mainWindowEl.style.removeProperty("--avatar-image-url");
955 } else if (state.status === UIState.STATUS_NOT_VERIFIED) {
956 stateValue = "unverified";
957 headerTitleL10nId = "account-finish-account-setup";
958 headerDescription = state.displayName || state.email;
959 } else if (state.status === UIState.STATUS_SIGNED_IN) {
960 stateValue = "signedin";
961 if (state.avatarURL && !state.avatarIsDefault) {
962 // The user has specified a custom avatar, attempt to load the image on all the menu buttons.
963 const bgImage = `url("${state.avatarURL}")`;
964 let img = new Image();
966 // If the image has successfully loaded, update the menu buttons else
967 // we will use the default avatar image.
968 mainWindowEl.style.setProperty("--avatar-image-url", bgImage);
970 img.onerror = () => {
971 // If the image failed to load, remove the property and default
972 // to standard avatar.
973 mainWindowEl.style.removeProperty("--avatar-image-url");
975 img.src = state.avatarURL;
976 signedInContainer.hidden = false;
977 menuHeaderDescriptionEl.hidden = false;
979 mainWindowEl.style.removeProperty("--avatar-image-url");
982 cadButtonEl.removeAttribute("disabled");
984 if (state.syncEnabled) {
985 syncNowButtonEl.removeAttribute("hidden");
986 syncSetupButtonEl.hidden = true;
989 headerTitleL10nId = "appmenuitem-fxa-manage-account";
990 headerDescription = state.displayName || state.email;
992 headerDescription = this.fluentStrings.formatValueSync(
993 "fxa-menu-turn-on-sync-default"
996 mainWindowEl.setAttribute("fxastatus", stateValue);
998 menuHeaderTitleEl.value =
999 this.fluentStrings.formatValueSync(headerTitleL10nId);
1000 // If we description is empty, we hide it
1001 menuHeaderDescriptionEl.hidden = !headerDescription;
1002 menuHeaderDescriptionEl.value = headerDescription;
1003 // We remove the data-l10n-id attribute here to prevent the node's value
1004 // attribute from being overwritten by Fluent when the panel is moved
1005 // around in the DOM.
1006 menuHeaderTitleEl.removeAttribute("data-l10n-id");
1007 menuHeaderDescriptionEl.removeAttribute("data-l10n-id");
1010 enableSendTabIfValidTab() {
1011 // All tabs selected must be sendable for the Send Tab button to be enabled
1013 let canSendAllURIs = gBrowser.selectedTabs.every(
1014 t => !!BrowserUtils.getShareableURL(t.linkedBrowser.currentURI)
1017 PanelMultiView.getViewNode(
1019 "PanelUI-fxa-menu-sendtab-button"
1020 ).hidden = !canSendAllURIs;
1023 emitFxaToolbarTelemetry(type, panel) {
1024 if (UIState.isReady() && panel) {
1025 const state = UIState.get();
1026 const hasAvatar = state.avatarURL && !state.avatarIsDefault;
1027 let extraOptions = {
1028 fxa_status: state.status,
1029 fxa_avatar: hasAvatar ? "true" : "false",
1032 // When the fxa avatar panel is within the Firefox app menu,
1033 // we emit different telemetry.
1034 let eventName = "fxa_avatar_menu";
1035 if (this.isPanelInsideAppMenu(panel)) {
1036 eventName = "fxa_app_menu";
1039 Services.telemetry.recordEvent(
1049 isPanelInsideAppMenu(panel = undefined) {
1050 const appMenuPanel = document.getElementById("appMenu-popup");
1051 if (panel && appMenuPanel.contains(panel)) {
1057 updatePanelPopup({ email, displayName, status }) {
1058 const appMenuStatus = PanelMultiView.getViewNode(
1060 "appMenu-fxa-status2"
1062 const appMenuLabel = PanelMultiView.getViewNode(
1064 "appMenu-fxa-label2"
1066 const appMenuHeaderText = PanelMultiView.getViewNode(
1070 const appMenuHeaderTitle = PanelMultiView.getViewNode(
1072 "appMenu-header-title"
1074 const appMenuHeaderDescription = PanelMultiView.getViewNode(
1076 "appMenu-header-description"
1078 const fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa");
1080 let defaultLabel = this.fluentStrings.formatValueSync(
1081 "appmenu-fxa-signed-in-label"
1083 // Reset the status bar to its original state.
1084 appMenuLabel.setAttribute("label", defaultLabel);
1085 appMenuLabel.removeAttribute("aria-labelledby");
1086 appMenuStatus.removeAttribute("fxastatus");
1088 if (status == UIState.STATUS_NOT_CONFIGURED) {
1089 appMenuHeaderText.hidden = false;
1090 appMenuStatus.classList.add("toolbaritem-combined-buttons");
1091 appMenuLabel.classList.remove("subviewbutton-nav");
1092 appMenuHeaderTitle.hidden = true;
1093 appMenuHeaderDescription.value = defaultLabel;
1096 appMenuLabel.classList.remove("subviewbutton-nav");
1098 appMenuHeaderText.hidden = true;
1099 appMenuStatus.classList.remove("toolbaritem-combined-buttons");
1101 // While we prefer the display name in most case, in some strings
1102 // where the context is something like "Verify %s", the email
1103 // is used even when there's a display name.
1104 if (status == UIState.STATUS_LOGIN_FAILED) {
1105 const [tooltipDescription, errorLabel] =
1106 this.fluentStrings.formatValuesSync([
1107 { id: "account-reconnect", args: { email } },
1108 { id: "account-disconnected2" },
1110 appMenuStatus.setAttribute("fxastatus", "login-failed");
1111 appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
1112 appMenuLabel.classList.add("subviewbutton-nav");
1113 appMenuHeaderTitle.hidden = false;
1114 appMenuHeaderTitle.value = errorLabel;
1115 appMenuHeaderDescription.value = displayName || email;
1117 appMenuLabel.removeAttribute("label");
1118 appMenuLabel.setAttribute(
1120 `${appMenuHeaderTitle.id},${appMenuHeaderDescription.id}`
1123 } else if (status == UIState.STATUS_NOT_VERIFIED) {
1124 const [tooltipDescription, unverifiedLabel] =
1125 this.fluentStrings.formatValuesSync([
1126 { id: "account-verify", args: { email } },
1127 { id: "account-finish-account-setup" },
1129 appMenuStatus.setAttribute("fxastatus", "unverified");
1130 appMenuStatus.setAttribute("tooltiptext", tooltipDescription);
1131 appMenuLabel.classList.add("subviewbutton-nav");
1132 appMenuHeaderTitle.hidden = false;
1133 appMenuHeaderTitle.value = unverifiedLabel;
1134 appMenuHeaderDescription.value = email;
1136 appMenuLabel.removeAttribute("label");
1137 appMenuLabel.setAttribute(
1139 `${appMenuHeaderTitle.id},${appMenuHeaderDescription.id}`
1144 appMenuHeaderTitle.hidden = true;
1145 appMenuHeaderDescription.value = displayName || email;
1146 appMenuStatus.setAttribute("fxastatus", "signedin");
1147 appMenuLabel.setAttribute("label", displayName || email);
1148 appMenuLabel.classList.add("subviewbutton-nav");
1149 fxaPanelView.setAttribute(
1151 this.fluentStrings.formatValueSync("appmenu-account-header")
1153 appMenuStatus.removeAttribute("tooltiptext");
1156 updateState(state) {
1157 for (let [shown, menuId, boxId] of [
1159 state.status == UIState.STATUS_NOT_CONFIGURED,
1161 "PanelUI-remotetabs-setupsync",
1164 state.status == UIState.STATUS_SIGNED_IN && !state.syncEnabled,
1166 "PanelUI-remotetabs-syncdisabled",
1169 state.status == UIState.STATUS_LOGIN_FAILED,
1171 "PanelUI-remotetabs-reauthsync",
1174 state.status == UIState.STATUS_NOT_VERIFIED,
1175 "sync-unverifieditem",
1176 "PanelUI-remotetabs-unverified",
1179 state.status == UIState.STATUS_SIGNED_IN && state.syncEnabled,
1181 "PanelUI-remotetabs-main",
1184 document.getElementById(menuId).hidden = PanelMultiView.getViewNode(
1191 updateSyncStatus(state) {
1193 document.querySelector(".syncNowBtn") ||
1195 .getElementById("appMenu-viewCache")
1196 .content.querySelector(".syncNowBtn");
1197 const syncingUI = syncNow.getAttribute("syncstatus") == "active";
1198 if (state.syncing != syncingUI) {
1199 // Do we need to update the UI?
1200 state.syncing ? this.onActivityStart() : this.onActivityStop();
1204 async openSignInAgainPage(entryPoint) {
1205 if (!(await FxAccounts.canConnectAccount())) {
1208 const url = await FxAccounts.config.promiseForceSigninURI(entryPoint);
1209 switchToTabHavingURI(url, true, {
1210 replaceQueryString: true,
1211 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1215 async openDevicesManagementPage(entryPoint) {
1216 let url = await FxAccounts.config.promiseManageDevicesURI(entryPoint);
1217 switchToTabHavingURI(url, true, {
1218 replaceQueryString: true,
1219 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1223 async openConnectAnotherDevice(entryPoint) {
1224 const url = await FxAccounts.config.promiseConnectDeviceURI(entryPoint);
1225 openTrustedLinkIn(url, "tab");
1228 async openConnectAnotherDeviceFromFxaMenu(panel = undefined) {
1229 this.emitFxaToolbarTelemetry("cad", panel);
1230 let entryPoint = "fxa_discoverability_native";
1231 if (this.isPanelInsideAppMenu(panel)) {
1232 entryPoint = "fxa_app_menu";
1234 this.openConnectAnotherDevice(entryPoint);
1237 openSendToDevicePromo() {
1238 const url = Services.urlFormatter.formatURLPref(
1239 "identity.sendtabpromo.url"
1241 switchToTabHavingURI(url, true, { replaceQueryString: true });
1244 async clickFxAMenuHeaderButton(panel = undefined) {
1245 // Depending on the current logged in state of a user,
1246 // clicking the FxA header will either open
1247 // a sign-in page, account management page, or sync
1248 // preferences page.
1249 const { status } = UIState.get();
1251 case UIState.STATUS_NOT_CONFIGURED:
1252 this.openFxAEmailFirstPageFromFxaMenu(panel);
1254 case UIState.STATUS_LOGIN_FAILED:
1255 this.openPrefsFromFxaMenu("sync_settings", panel);
1257 case UIState.STATUS_NOT_VERIFIED:
1258 this.openFxAEmailFirstPage("fxa_app_menu_reverify");
1260 case UIState.STATUS_SIGNED_IN:
1261 this.openFxAManagePageFromFxaMenu(panel);
1265 async openFxAEmailFirstPage(entryPoint, extraParams = {}) {
1266 if (!(await FxAccounts.canConnectAccount())) {
1269 const url = await FxAccounts.config.promiseConnectAccountURI(
1273 switchToTabHavingURI(url, true, { replaceQueryString: true });
1276 async openFxAEmailFirstPageFromFxaMenu(panel = undefined, extraParams = {}) {
1277 this.emitFxaToolbarTelemetry("login", panel);
1278 let entryPoint = "fxa_discoverability_native";
1280 entryPoint = "fxa_toolbar_button";
1282 this.openFxAEmailFirstPage(entryPoint, extraParams);
1285 async openFxAManagePage(entryPoint) {
1286 const url = await FxAccounts.config.promiseManageURI(entryPoint);
1287 switchToTabHavingURI(url, true, { replaceQueryString: true });
1290 async openFxAManagePageFromFxaMenu(panel = undefined) {
1291 this.emitFxaToolbarTelemetry("account_settings", panel);
1292 let entryPoint = "fxa_discoverability_native";
1293 if (this.isPanelInsideAppMenu(panel)) {
1294 entryPoint = "fxa_app_menu";
1296 this.openFxAManagePage(entryPoint);
1299 // Returns true if we managed to send the tab to any targets, false otherwise.
1300 async sendTabToDevice(url, targets, title) {
1301 const fxaCommandsDevices = [];
1302 for (const target of targets) {
1303 if (fxAccounts.commands.sendTab.isDeviceCompatible(target)) {
1304 fxaCommandsDevices.push(target);
1306 this.log.error(`Target ${target.id} unsuitable for send tab.`);
1309 // If a primary-password is enabled then it must be unlocked so FxA can get
1310 // the encryption keys from the login manager. (If we end up using the "sync"
1311 // fallback that would end up prompting by itself, but the FxA command route
1312 // will not) - so force that here.
1313 let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService(
1314 Ci.nsILoginManagerCrypto
1316 if (!cryptoSDR.isLoggedIn) {
1317 if (cryptoSDR.uiBusy) {
1318 this.log.info("Master password UI is busy - not sending the tabs");
1322 cryptoSDR.encrypt("bacon"); // forces the mp prompt.
1325 "Master password remains unlocked - not sending the tabs"
1331 if (fxaCommandsDevices.length) {
1333 `Sending a tab to ${fxaCommandsDevices
1335 .join(", ")} using FxA commands.`
1337 const report = await fxAccounts.commands.sendTab.send(
1341 for (let { device, error } of report.failed) {
1343 `Failed to send a tab with FxA commands for ${device.id}.`,
1349 return numFailed < targets.length; // Good enough.
1352 populateSendTabToDevicesMenu(
1360 uri = BrowserUtils.getShareableURL(uri);
1362 // log an error as everyone should have already checked this.
1363 this.log.error("Ignoring request to share a non-sharable URL");
1366 if (!createDeviceNodeFn) {
1367 createDeviceNodeFn = (targetId, name) => {
1368 let eltName = name ? "menuitem" : "menuseparator";
1369 return document.createXULElement(eltName);
1373 // remove existing menu items
1374 for (let i = devicesPopup.children.length - 1; i >= 0; --i) {
1375 let child = devicesPopup.children[i];
1376 if (child.classList.contains("sync-menuitem")) {
1381 if (gSync.sendTabConfiguredAndLoading) {
1382 // We can only be in this case in the page action menu.
1386 const fragment = document.createDocumentFragment();
1388 const state = UIState.get();
1389 if (state.status == UIState.STATUS_SIGNED_IN) {
1390 const targets = this.getSendTabTargets();
1391 if (targets.length) {
1392 this._appendSendTabDeviceList(
1402 this._appendSendTabSingleDevice(fragment, createDeviceNodeFn);
1405 state.status == UIState.STATUS_NOT_VERIFIED ||
1406 state.status == UIState.STATUS_LOGIN_FAILED
1408 this._appendSendTabVerify(fragment, createDeviceNodeFn);
1410 // The only status not handled yet is STATUS_NOT_CONFIGURED, and
1411 // when we're in that state, none of the menus that call
1412 // populateSendTabToDevicesMenu are available, so entering this
1413 // state is unexpected.
1415 "Called populateSendTabToDevicesMenu when in STATUS_NOT_CONFIGURED " +
1420 devicesPopup.appendChild(fragment);
1423 _appendSendTabDeviceList(
1432 let tabsToSend = multiselected
1433 ? gBrowser.selectedTabs.map(t => {
1435 url: t.linkedBrowser.currentURI.spec,
1436 title: t.linkedBrowser.contentTitle,
1441 const send = to => {
1444 // sendTabToDevice does not reject.
1445 this.sendTabToDevice(t.url, to, t.title)
1448 // Show the Sent! confirmation if any of the sends succeeded.
1449 if (results.includes(true)) {
1450 // FxA button could be hidden with CSS since the user is logged out,
1451 // although it seems likely this would only happen in testing...
1452 let fxastatus = document.documentElement.getAttribute("fxastatus");
1455 fxastatus != "not_configured" &&
1456 document.getElementById("fxa-toolbar-menu-button")?.parentNode
1457 ?.id != "widget-overflow-list" &&
1458 document.getElementById("fxa-toolbar-menu-button")) ||
1459 document.getElementById("PanelUI-menu-button");
1460 ConfirmationHint.show(anchorNode, "confirmation-hint-send-to-device");
1462 fxAccounts.flushLogFile();
1465 const onSendAllCommand = () => {
1468 const onTargetDeviceCommand = event => {
1469 const targetId = event.target.getAttribute("clientId");
1470 const target = targets.find(t => t.id == targetId);
1474 function addTargetDevice(targetId, name, targetType, lastModified) {
1475 const targetDevice = createDeviceNodeFn(
1481 targetDevice.addEventListener(
1483 targetId ? onTargetDeviceCommand : onSendAllCommand,
1486 targetDevice.classList.add("sync-menuitem", "sendtab-target");
1487 targetDevice.setAttribute("clientId", targetId);
1488 targetDevice.setAttribute("clientType", targetType);
1489 targetDevice.setAttribute("label", name);
1490 fragment.appendChild(targetDevice);
1493 for (let target of targets) {
1494 let type, lastModified;
1495 if (target.clientRecord) {
1496 type = Weave.Service.clientsEngine.getClientType(
1497 target.clientRecord.id
1499 lastModified = new Date(target.clientRecord.serverLastModified * 1000);
1501 // For phones, FxA uses "mobile" and Sync clients uses "phone".
1502 type = target.type == "mobile" ? "phone" : target.type;
1503 lastModified = target.lastAccessTime
1504 ? new Date(target.lastAccessTime)
1507 addTargetDevice(target.id, target.name, type, lastModified);
1510 if (targets.length > 1) {
1511 // "Send to All Devices" menu item
1512 const separator = createDeviceNodeFn();
1513 separator.classList.add("sync-menuitem");
1514 fragment.appendChild(separator);
1515 const [allDevicesLabel, manageDevicesLabel] =
1516 this.fluentStrings.formatValuesSync(
1518 ? ["account-send-to-all-devices", "account-manage-devices"]
1520 "account-send-to-all-devices-titlecase",
1521 "account-manage-devices-titlecase",
1524 addTargetDevice("", allDevicesLabel, "");
1526 // "Manage devices" menu item
1527 // We piggyback on the createDeviceNodeFn implementation,
1528 // it's a big disgusting.
1529 const targetDevice = createDeviceNodeFn(
1535 targetDevice.addEventListener(
1537 () => gSync.openDevicesManagementPage("sendtab"),
1540 targetDevice.classList.add("sync-menuitem", "sendtab-target");
1541 targetDevice.setAttribute("label", manageDevicesLabel);
1542 fragment.appendChild(targetDevice);
1546 _appendSendTabSingleDevice(fragment, createDeviceNodeFn) {
1547 const [noDevices, learnMore, connectDevice] =
1548 this.fluentStrings.formatValuesSync([
1549 "account-send-tab-to-device-singledevice-status",
1550 "account-send-tab-to-device-singledevice-learnmore",
1551 "account-send-tab-to-device-connectdevice",
1555 label: connectDevice,
1556 command: () => this.openConnectAnotherDevice("sendtab"),
1558 { label: learnMore, command: () => this.openSendToDevicePromo() },
1560 this._appendSendTabInfoItems(
1568 _appendSendTabVerify(fragment, createDeviceNodeFn) {
1569 const [notVerified, verifyAccount] = this.fluentStrings.formatValuesSync([
1570 "account-send-tab-to-device-verify-status",
1571 "account-send-tab-to-device-verify",
1574 { label: verifyAccount, command: () => this.openPrefs("sendtab") },
1576 this._appendSendTabInfoItems(
1584 _appendSendTabInfoItems(fragment, createDeviceNodeFn, statusLabel, actions) {
1585 const status = createDeviceNodeFn(null, statusLabel, null);
1586 status.setAttribute("label", statusLabel);
1587 status.setAttribute("disabled", true);
1588 status.classList.add("sync-menuitem");
1589 fragment.appendChild(status);
1591 const separator = createDeviceNodeFn(null, null, null);
1592 separator.classList.add("sync-menuitem");
1593 fragment.appendChild(separator);
1595 for (let { label, command } of actions) {
1596 const actionItem = createDeviceNodeFn(null, label, null);
1597 actionItem.addEventListener("command", command, true);
1598 actionItem.classList.add("sync-menuitem");
1599 actionItem.setAttribute("label", label);
1600 fragment.appendChild(actionItem);
1604 // "Send Tab to Device" menu item
1605 updateTabContextMenu(aPopupMenu, aTargetTab) {
1606 // We may get here before initialisation. This situation
1607 // can lead to a empty label for 'Send To Device' Menu.
1610 if (!this.FXA_ENABLED) {
1611 // These items are hidden in onFxaDisabled(). No need to do anything.
1614 let hasASendableURI = false;
1615 for (let tab of aTargetTab.multiselected
1616 ? gBrowser.selectedTabs
1618 if (BrowserUtils.getShareableURL(tab.linkedBrowser.currentURI)) {
1619 hasASendableURI = true;
1623 const enabled = !this.sendTabConfiguredAndLoading && hasASendableURI;
1624 const hideItems = this.shouldHideSendContextMenuItems(enabled);
1626 let sendTabsToDevice = document.getElementById("context_sendTabToDevice");
1627 sendTabsToDevice.disabled = !enabled;
1629 if (hideItems || !hasASendableURI) {
1630 sendTabsToDevice.hidden = true;
1632 let tabCount = aTargetTab.multiselected
1633 ? gBrowser.multiSelectedTabsCount
1635 sendTabsToDevice.setAttribute(
1637 JSON.stringify({ tabCount })
1639 sendTabsToDevice.hidden = false;
1643 // "Send Page to Device" and "Send Link to Device" menu items
1644 updateContentContextMenu(contextMenu) {
1645 if (!this.FXA_ENABLED) {
1646 // These items are hidden by default. No need to do anything.
1649 // showSendLink and showSendPage are mutually exclusive
1650 const showSendLink =
1651 contextMenu.onSaveableLink || contextMenu.onPlainTextLink;
1652 const showSendPage =
1655 contextMenu.isContentSelected ||
1656 contextMenu.onImage ||
1657 contextMenu.onCanvas ||
1658 contextMenu.onVideo ||
1659 contextMenu.onAudio ||
1660 contextMenu.onLink ||
1661 contextMenu.onTextInput
1664 const targetURI = showSendLink
1665 ? contextMenu.getLinkURI()
1666 : contextMenu.browser.currentURI;
1668 !this.sendTabConfiguredAndLoading &&
1669 BrowserUtils.getShareableURL(targetURI);
1670 const hideItems = this.shouldHideSendContextMenuItems(enabled);
1672 contextMenu.showItem(
1673 "context-sendpagetodevice",
1674 !hideItems && showSendPage
1676 contextMenu.showItem(
1677 "context-sendlinktodevice",
1678 !hideItems && showSendLink
1681 if (!showSendLink && !showSendPage) {
1685 contextMenu.setItemAttr(
1686 showSendPage ? "context-sendpagetodevice" : "context-sendlinktodevice",
1690 // return true if context menu items are visible
1691 return !hideItems && (showSendPage || showSendLink);
1694 // Functions called by observers
1696 this._isCurrentlySyncing = true;
1697 clearTimeout(this._syncAnimationTimer);
1698 this._syncStartTime = Date.now();
1700 document.querySelectorAll(".syncnow-label").forEach(el => {
1701 let l10nId = el.getAttribute("syncing-data-l10n-id");
1702 document.l10n.setAttributes(el, l10nId);
1705 document.querySelectorAll(".syncNowBtn").forEach(el => {
1706 el.setAttribute("syncstatus", "active");
1710 .getElementById("appMenu-viewCache")
1711 .content.querySelectorAll(".syncNowBtn")
1713 el.setAttribute("syncstatus", "active");
1718 this._isCurrentlySyncing = false;
1723 document.querySelectorAll(".syncnow-label").forEach(el => {
1724 let l10nId = el.getAttribute("sync-now-data-l10n-id");
1725 document.l10n.setAttributes(el, l10nId);
1728 document.querySelectorAll(".syncNowBtn").forEach(el => {
1729 el.removeAttribute("syncstatus");
1733 .getElementById("appMenu-viewCache")
1734 .content.querySelectorAll(".syncNowBtn")
1736 el.removeAttribute("syncstatus");
1739 Services.obs.notifyObservers(null, "test:browser-sync:activity-stop");
1743 let now = Date.now();
1744 let syncDuration = now - this._syncStartTime;
1746 if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
1747 let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
1748 clearTimeout(this._syncAnimationTimer);
1749 this._syncAnimationTimer = setTimeout(
1750 () => this._onActivityStop(),
1754 this._onActivityStop();
1758 // Disconnect from sync, and optionally disconnect from the FxA account.
1759 // Returns true if the disconnection happened (ie, if the user didn't decline
1760 // when asked to confirm)
1761 async disconnect({ confirm = true, disconnectAccount = true } = {}) {
1762 if (disconnectAccount) {
1763 let deleteLocalData = false;
1765 let options = await this._confirmFxaAndSyncDisconnect();
1766 if (!options.userConfirmedDisconnect) {
1769 deleteLocalData = options.deleteLocalData;
1771 return this._disconnectFxaAndSync(deleteLocalData);
1774 if (confirm && !(await this._confirmSyncDisconnect())) {
1777 return this._disconnectSync();
1780 // Prompt the user to confirm disconnect from FxA and sync with the option
1781 // to delete syncable data from the device.
1782 async _confirmFxaAndSyncDisconnect() {
1784 userConfirmedDisconnect: false,
1785 deleteLocalData: false,
1788 let [title, body, button, checkbox] = await document.l10n.formatValues([
1789 { id: "fxa-signout-dialog-title2" },
1790 { id: "fxa-signout-dialog-body" },
1791 { id: "fxa-signout-dialog2-button" },
1792 { id: "fxa-signout-dialog2-checkbox" },
1796 Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
1797 Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1;
1799 if (!UIState.get().syncEnabled) {
1803 const result = await Services.prompt.asyncConfirmEx(
1804 window.browsingContext,
1805 Services.prompt.MODAL_TYPE_INTERNAL_WINDOW,
1815 const propBag = result.QueryInterface(Ci.nsIPropertyBag2);
1816 options.userConfirmedDisconnect = propBag.get("buttonNumClicked") == 0;
1817 options.deleteLocalData = propBag.get("checked");
1822 async _disconnectFxaAndSync(deleteLocalData) {
1823 const { SyncDisconnect } = ChromeUtils.importESModule(
1824 "resource://services-sync/SyncDisconnect.sys.mjs"
1826 // Record telemetry.
1827 await fxAccounts.telemetry.recordDisconnection(null, "ui");
1829 await SyncDisconnect.disconnect(deleteLocalData).catch(e => {
1830 console.error("Failed to disconnect.", e);
1836 // Prompt the user to confirm disconnect from sync. In this case the data
1837 // on the device is not deleted.
1838 async _confirmSyncDisconnect() {
1839 const [title, body, button] = await document.l10n.formatValues([
1840 { id: `sync-disconnect-dialog-title2` },
1841 { id: `sync-disconnect-dialog-body` },
1842 { id: "sync-disconnect-dialog-button" },
1846 Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
1847 Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1;
1849 // buttonPressed will be 0 for disconnect, 1 for cancel.
1850 const buttonPressed = Services.prompt.confirmEx(
1861 return buttonPressed == 0;
1864 async _disconnectSync() {
1865 await fxAccounts.telemetry.recordDisconnection("sync", "ui");
1867 await Weave.Service.promiseInitialized;
1868 await Weave.Service.startOver();
1873 // doSync forces a sync - it *does not* return a promise as it is called
1874 // via the various UI components.
1876 if (!UIState.isReady()) {
1879 // Note we don't bother checking if sync is actually enabled - none of the
1880 // UI which calls this function should be visible in that case.
1881 const state = UIState.get();
1882 if (state.status == UIState.STATUS_SIGNED_IN) {
1883 this.updateSyncStatus({ syncing: true });
1884 Services.tm.dispatchToMainThread(() => {
1885 // We are pretty confident that push helps us pick up all FxA commands,
1886 // but some users might have issues with push, so let's unblock them
1887 // by fetching the missed FxA commands on manual sync.
1888 fxAccounts.commands.pollDeviceCommands().catch(e => {
1889 this.log.error("Fetching missed remote commands failed.", e);
1891 Weave.Service.sync();
1896 doSyncFromFxaMenu(panel) {
1898 this.emitFxaToolbarTelemetry("sync_now", panel);
1901 openPrefs(entryPoint = "syncbutton", origin = undefined) {
1902 window.openPreferences("paneSync", {
1904 urlParams: { entrypoint: entryPoint },
1908 openPrefsFromFxaMenu(type, panel) {
1909 this.emitFxaToolbarTelemetry(type, panel);
1910 let entryPoint = "fxa_discoverability_native";
1911 if (this.isPanelInsideAppMenu(panel)) {
1912 entryPoint = "fxa_app_menu";
1914 this.openPrefs(entryPoint);
1917 openPrefsFromFxaButton(type, panel) {
1918 let entryPoint = "fxa_toolbar_button_sync";
1919 this.emitFxaToolbarTelemetry(type, panel);
1920 this.openPrefs(entryPoint);
1923 openSyncedTabsPanel() {
1924 let placement = CustomizableUI.getPlacementOfWidget("sync-button");
1925 let area = placement?.area;
1926 let anchor = document.getElementById("sync-button");
1927 if (area == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
1928 // The button is in the overflow panel, so we need to show the panel,
1929 // then show our subview.
1930 let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR);
1931 navbar.overflowable.show().then(() => {
1932 PanelUI.showSubView("PanelUI-remotetabs", anchor);
1936 !anchor?.checkVisibility({ checkVisibilityCSS: true, flush: false })
1938 anchor = document.getElementById("PanelUI-menu-button");
1940 // It is placed somewhere else - just try and show it.
1941 PanelUI.showSubView("PanelUI-remotetabs", anchor);
1945 refreshSyncButtonsTooltip() {
1946 const state = UIState.get();
1947 this.updateSyncButtonsTooltip(state);
1950 /* Update the tooltip for the sync icon in the main menu and in Synced Tabs.
1951 If Sync is configured, the tooltip is when the last sync occurred,
1952 otherwise the tooltip reflects the fact that Sync needs to be
1955 updateSyncButtonsTooltip(state) {
1956 // Sync buttons are 1/2 Sync related and 1/2 FxA related
1957 let l10nId, l10nArgs;
1958 switch (state.status) {
1959 case UIState.STATUS_NOT_VERIFIED:
1960 // "needs verification"
1961 l10nId = "account-verify";
1962 l10nArgs = { email: state.email };
1964 case UIState.STATUS_LOGIN_FAILED:
1965 // "need to reconnect/re-enter your password"
1966 l10nId = "account-reconnect";
1967 l10nArgs = { email: state.email };
1969 case UIState.STATUS_NOT_CONFIGURED:
1970 // Button is not shown in this state
1973 // Sync appears configured - format the "last synced at" time.
1974 let lastSyncDate = this.formatLastSyncDate(state.lastSync);
1976 l10nId = "appmenu-fxa-last-sync";
1977 l10nArgs = { time: lastSyncDate };
1981 const tooltiptext = l10nId
1982 ? this.fluentStrings.formatValueSync(l10nId, l10nArgs)
1986 "PanelUI-remotetabs-syncnow",
1987 "PanelUI-fxa-menu-syncnow-button",
1989 syncNowBtns.forEach(id => {
1990 let el = PanelMultiView.getViewNode(document, id);
1992 el.setAttribute("tooltiptext", tooltiptext);
1994 el.removeAttribute("tooltiptext");
1999 get relativeTimeFormat() {
2000 delete this.relativeTimeFormat;
2001 return (this.relativeTimeFormat = new Services.intl.RelativeTimeFormat(
2007 formatLastSyncDate(date) {
2009 // Date can be null before the first sync!
2013 let adjustedDate = new Date(Date.now() - 1000);
2014 let relativeDateStr = this.relativeTimeFormat.formatBestUnit(
2015 date < adjustedDate ? date : adjustedDate
2017 return relativeDateStr;
2019 // shouldn't happen, but one client having an invalid date shouldn't
2020 // break the entire feature.
2021 this.log.warn("failed to format lastSync time", date, ex);
2027 // Note that this element is only shown if Sync is enabled.
2028 let element = PanelMultiView.getViewNode(
2030 "PanelUI-remotetabs-main"
2033 if (Weave.Service.clientsEngine.stats.numClients > 1) {
2034 element.setAttribute("devices-status", "multi");
2036 element.setAttribute("devices-status", "single");
2042 document.documentElement.setAttribute("fxadisabled", true);
2044 const toHide = [...document.querySelectorAll(".sync-ui-item")];
2045 for (const item of toHide) {
2050 // This should only be shown if we have enabled the pxiPanel via
2051 // an experiment or explicitly through prefs
2053 const mainPanelEl = PanelMultiView.getViewNode(
2055 "PanelUI-fxa-cta-menu"
2058 const syncCtaEl = PanelMultiView.getViewNode(
2060 "PanelUI-fxa-menu-sync-button"
2062 // If we're not in the experiment then we do not enable this at all
2063 if (!this.PXI_TOOLBAR_ENABLED) {
2064 // If we've previously shown this but got disabled
2065 // we should ensure we hide the panel
2066 mainPanelEl.hidden = true;
2070 // If we're already signed in an syncing, we shouldn't show the sync CTA
2071 syncCtaEl.hidden = this.isSignedIn;
2074 let monitorPanelEl = PanelMultiView.getViewNode(
2076 "PanelUI-fxa-menu-monitor-button"
2078 let monitorEnabled = Services.prefs.getBoolPref(
2079 "identity.fxaccounts.toolbar.pxiToolbarEnabled.monitorEnabled",
2082 monitorPanelEl.hidden = !monitorEnabled;
2085 let relayPanelEl = PanelMultiView.getViewNode(
2087 "PanelUI-fxa-menu-relay-button"
2090 BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.RELAY) &&
2091 Services.prefs.getBoolPref(
2092 "identity.fxaccounts.toolbar.pxiToolbarEnabled.relayEnabled",
2095 relayPanelEl.hidden = !relayEnabled;
2098 let VpnPanelEl = PanelMultiView.getViewNode(
2100 "PanelUI-fxa-menu-vpn-button"
2103 BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.VPN) &&
2104 Services.prefs.getBoolPref(
2105 "identity.fxaccounts.toolbar.pxiToolbarEnabled.vpnEnabled",
2108 VpnPanelEl.hidden = !vpnEnabled;
2110 // We should only the show the separator if we have at least one CTA enabled
2111 PanelMultiView.getViewNode(document, "PanelUI-products-separator").hidden =
2112 !monitorEnabled && !relayEnabled && !vpnEnabled;
2113 mainPanelEl.hidden = false;
2115 async openMonitorLink(panel) {
2116 this.emitFxaToolbarTelemetry("monitor_cta", panel);
2117 await this.openCtaLink(
2118 FX_MONITOR_OAUTH_CLIENT_ID,
2119 new URL("https://monitor.firefox.com"),
2120 new URL("https://monitor.firefox.com/user/breaches")
2124 async openRelayLink(panel) {
2125 this.emitFxaToolbarTelemetry("relay_cta", panel);
2126 await this.openCtaLink(
2127 FX_RELAY_OAUTH_CLIENT_ID,
2128 new URL("https://relay.firefox.com"),
2129 new URL("https://relay.firefox.com/accounts/profile")
2133 async openVPNLink(panel) {
2134 this.emitFxaToolbarTelemetry("vpn_cta", panel);
2135 await this.openCtaLink(
2136 VPN_OAUTH_CLIENT_ID,
2137 new URL("https://www.mozilla.org/en-US/products/vpn/"),
2138 new URL("https://www.mozilla.org/en-US/products/vpn/")
2142 // A generic opening based on
2143 async openCtaLink(clientId, defaultUrl, signedInUrl) {
2145 utm_medium: "firefox-desktop",
2146 utm_source: "toolbar",
2147 utm_campaign: "discovery",
2149 const searchParams = new URLSearchParams(params);
2151 if (!this.isSignedIn) {
2152 // Add the base params + not signed in
2153 defaultUrl.search = searchParams.toString();
2154 defaultUrl.searchParams.append("utm_content", "notsignedin");
2155 this.openLink(defaultUrl);
2160 // Note: This is a network call
2161 let attachedClients = await fxAccounts.listAttachedOAuthClients();
2162 // If we have at least one client based on clientId passed in
2163 let hasPXIClient = attachedClients.some(c => !!c.id && c.id === clientId);
2165 const url = hasPXIClient ? signedInUrl : defaultUrl;
2166 // Add base params + signed in
2167 url.search = searchParams.toString();
2168 url.searchParams.append("utm_content", "signedIn");
2175 switchToTabHavingURI(url, true, { replaceQueryString: true });
2178 QueryInterface: ChromeUtils.generateQI([
2180 "nsISupportsWeakReference",