Bug 1874684 - Part 4: Prefer const references instead of copying Instant values....
[gecko.git] / browser / base / content / browser-sync.js
blob065546d7b8ae55ca4c17ff47f9588ae849fe4e3d
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 */
8 const {
9   FX_MONITOR_OAUTH_CLIENT_ID,
10   FX_RELAY_OAUTH_CLIENT_ID,
11   VPN_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",
28 });
30 const MIN_STATUS_ANIMATION_DURATION = 1600;
32 this.SyncedTabsPanelList = class SyncedTabsPanelList {
33   static sRemoteTabsDeckIndices = {
34     DECKINDEX_TABS: 0,
35     DECKINDEX_FETCHING: 1,
36     DECKINDEX_TABSDISABLED: 2,
37     DECKINDEX_NOCLIENTS: 3,
38   };
40   static sRemoteTabsPerPage = 25;
41   static sRemoteTabsNextPageMinTabs = 5;
43   constructor(panelview, deck, tabsList, separator) {
44     this.QueryInterface = ChromeUtils.generateQI([
45       "nsIObserver",
46       "nsISupportsWeakReference",
47     ]);
49     Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED, true);
50     this.deck = deck;
51     this.tabsList = tabsList;
52     this.separator = separator;
53     this._showSyncedTabsPromise = Promise.resolve();
55     this.createSyncedTabs();
56   }
58   observe(subject, topic) {
59     if (topic == SyncedTabs.TOPIC_TABS_CHANGED) {
60       this._showSyncedTabs();
61     }
62   }
64   createSyncedTabs() {
65     if (SyncedTabs.isConfiguredToSyncTabs) {
66       if (SyncedTabs.hasSyncedThisSession) {
67         this.deck.selectedIndex =
68           SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABS;
69       } else {
70         // Sync hasn't synced tabs yet, so show the "fetching" panel.
71         this.deck.selectedIndex =
72           SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_FETCHING;
73       }
74       // force a background sync.
75       SyncedTabs.syncTabs().catch(ex => {
76         console.error(ex);
77       });
78       this.deck.toggleAttribute("syncingtabs", true);
79       // show the current list - it will be updated by our observer.
80       this._showSyncedTabs();
81       if (this.separator) {
82         this.separator.hidden = false;
83       }
84     } else {
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);
89       if (this.separator) {
90         this.separator.hidden = true;
91       }
92     }
93   }
95   // Update the synced tab list after any existing in-flight updates are complete.
96   _showSyncedTabs(paginationInfo) {
97     this._showSyncedTabsPromise = this._showSyncedTabsPromise.then(
98       () => {
99         return this.__showSyncedTabs(paginationInfo);
100       },
101       e => {
102         console.error(e);
103       }
104     );
105   }
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.
112       return undefined;
113     }
114     return SyncedTabs.getTabClients()
115       .then(clients => {
116         let noTabs = !UIState.get().syncEnabled || !clients.length;
117         this.deck.toggleAttribute("syncingtabs", !noTabs);
118         if (this.separator) {
119           this.separator.hidden = noTabs;
120         }
122         // The view may have been hidden while the promise was resolving.
123         if (!this.tabsList) {
124           return;
125         }
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.
129           return;
130         }
132         if (clients.length === 0) {
133           this.deck.selectedIndex =
134             SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_NOCLIENTS;
135           return;
136         }
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);
149           }
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
159               ? paginationInfo
160               : { clientId: client.id };
161           this._appendSyncClient(
162             client,
163             container,
164             labelId,
165             clientPaginationInfo
166           );
167           fragment.appendChild(container);
168         }
169         this.tabsList.appendChild(fragment);
170       })
171       .catch(err => {
172         console.error(err);
173       })
174       .then(() => {
175         // an observer for tests.
176         Services.obs.notifyObservers(
177           null,
178           "synced-tabs-menu:test:tabs-updated"
179         );
180       });
181   }
183   _clearSyncedTabList() {
184     let list = this.tabsList;
185     while (list.lastChild) {
186       list.lastChild.remove();
187     }
188   }
190   _createNoSyncedTabsElement(messageAttr, appendTo = null) {
191     if (!appendTo) {
192       appendTo = this.tabsList;
193     }
195     let messageLabel = document.createXULElement("label");
196     document.l10n.setAttributes(
197       messageLabel,
198       this.tabsList.getAttribute(messageAttr)
199     );
200     appendTo.appendChild(messageLabel);
201     return messageLabel;
202   }
204   _appendSyncClient(client, container, labelId, paginationInfo) {
205     let {
206       maxTabs = SyncedTabsPanelList.sRemoteTabsPerPage,
207       showInactive = false,
208     } = paginationInfo;
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(
214       "tooltiptext",
215       gSync.fluentStrings.formatValueSync("appmenu-fxa-last-sync", {
216         time: gSync.formatLastSyncDate(new Date(client.lastModified)),
217       })
218     );
219     clientItem.textContent = client.name;
221     container.appendChild(clientItem);
223     if (!client.tabs.length) {
224       let label = this._createNoSyncedTabsElement(
225         "notabsforclientlabel",
226         container
227       );
228       label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label");
229     } else {
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 =
237         hasNextPage &&
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
242         maxTabs = Math.min(
243           tabs.length - SyncedTabsPanelList.sRemoteTabsNextPageMinTabs,
244           maxTabs
245         );
246       }
247       if (hasNextPage) {
248         tabs = tabs.slice(0, maxTabs);
249       }
250       for (let [index, tab] of tabs.entries()) {
251         let tabEnt = this._createSyncedTabElement(tab, index);
252         container.appendChild(tabEnt);
253       }
254       if (numInactive) {
255         let elt = this._createShowInactiveTabsElement(
256           paginationInfo,
257           numInactive
258         );
259         container.appendChild(elt);
260       }
261       if (hasNextPage) {
262         let showAllEnt = this._createShowMoreSyncedTabsElement(paginationInfo);
263         container.appendChild(showAllEnt);
264       }
265     }
266   }
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);
274     item.setAttribute(
275       "label",
276       tabInfo.title != "" ? tabInfo.title : tabInfo.url
277     );
278     if (tabInfo.icon) {
279       item.setAttribute("image", tabInfo.icon);
280     }
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";
290       }
291       SyncedTabs.recordSyncedTabsTelemetry(object, "click", {
292         tab_pos: index.toString(),
293       });
294       document.defaultView.openUILink(tabInfo.url, e, {
295         triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
296           {}
297         ),
298       });
299       if (document.defaultView.whereToOpenLink(e) != "current") {
300         e.preventDefault();
301         e.stopPropagation();
302       } else {
303         CustomizableUI.hidePanelForNode(item);
304       }
305     });
306     return item;
307   }
309   _createShowMoreSyncedTabsElement(paginationInfo) {
310     let showMoreItem = document.createXULElement("toolbarbutton");
311     showMoreItem.setAttribute("itemtype", "showmorebutton");
312     showMoreItem.setAttribute("closemenu", "none");
313     showMoreItem.classList.add(
314       "subviewbutton",
315       "subviewbutton-nav",
316       "subviewbutton-nav-down"
317     );
318     document.l10n.setAttributes(showMoreItem, "appmenu-remote-tabs-showmore");
320     paginationInfo.maxTabs = Infinity;
321     showMoreItem.addEventListener("click", e => {
322       e.preventDefault();
323       e.stopPropagation();
324       this._showSyncedTabs(paginationInfo);
325     });
326     return showMoreItem;
327   }
329   _createShowInactiveTabsElement(paginationInfo, count) {
330     let showItem = document.createXULElement("toolbarbutton");
331     showItem.setAttribute("itemtype", "showmorebutton");
332     showItem.setAttribute("closemenu", "none");
333     showItem.classList.add(
334       "subviewbutton",
335       "subviewbutton-nav",
336       "subviewbutton-nav-down"
337     );
338     document.l10n.setAttributes(showItem, "appmenu-remote-tabs-showinactive");
339     document.l10n.setArgs(showItem, { count });
341     paginationInfo.showInactive = true;
342     showItem.addEventListener("click", e => {
343       e.preventDefault();
344       e.stopPropagation();
345       this._showSyncedTabs(paginationInfo);
346     });
347     return showItem;
348   }
350   destroy() {
351     Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED);
352     this.tabsList = null;
353     this.deck = null;
354     this.separator = null;
355   }
358 var gSync = {
359   _initialized: false,
360   _isCurrentlySyncing: false,
361   // The last sync start time. Used to calculate the leftover animation time
362   // once syncing completes (bug 1239042).
363   _syncStartTime: 0,
364   _syncAnimationTimer: 0,
365   _obs: ["weave:engine:sync:finish", "quit-application", UIState.ON_UPDATE],
367   get log() {
368     if (!this._log) {
369       const { Log } = ChromeUtils.importESModule(
370         "resource://gre/modules/Log.sys.mjs"
371       );
372       let syncLog = Log.repository.getLogger("Sync.Browser");
373       syncLog.manageLevelFromPref("services.sync.log.logger.browser");
374       this._log = syncLog;
375     }
376     return this._log;
377   },
379   get fluentStrings() {
380     delete this.fluentStrings;
381     return (this.fluentStrings = new Localization(
382       [
383         "branding/brand.ftl",
384         "browser/accounts.ftl",
385         "browser/appmenu.ftl",
386         "browser/sync.ftl",
387         "toolkit/branding/accounts.ftl",
388       ],
389       true
390     ));
391   },
393   // Returns true if FxA is configured, but the send tab targets list isn't
394   // ready yet.
395   get sendTabConfiguredAndLoading() {
396     return (
397       UIState.get().status == UIState.STATUS_SIGNED_IN &&
398       !fxAccounts.device.recentDeviceList
399     );
400   },
402   get isSignedIn() {
403     return UIState.get().status == UIState.STATUS_SIGNED_IN;
404   },
406   shouldHideSendContextMenuItems(enabled) {
407     const state = UIState.get();
408     // Only show the "Send..." context menu items when sending would be possible
409     if (
410       enabled &&
411       state.status == UIState.STATUS_SIGNED_IN &&
412       state.syncEnabled &&
413       this.getSendTabTargets().length
414     ) {
415       return false;
416     }
417     return true;
418   },
420   getSendTabTargets() {
421     const targets = [];
422     if (
423       UIState.get().status != UIState.STATUS_SIGNED_IN ||
424       !fxAccounts.device.recentDeviceList
425     ) {
426       return targets;
427     }
428     for (let d of fxAccounts.device.recentDeviceList) {
429       if (d.isCurrentDevice) {
430         continue;
431       }
433       if (fxAccounts.commands.sendTab.isDeviceCompatible(d)) {
434         targets.push(d);
435       }
436     }
437     return targets.sort((a, b) => b.lastAccessTime - a.lastAccessTime);
438   },
440   _definePrefGetters() {
441     XPCOMUtils.defineLazyPreferenceGetter(
442       this,
443       "FXA_ENABLED",
444       "identity.fxaccounts.enabled"
445     );
446     XPCOMUtils.defineLazyPreferenceGetter(
447       this,
448       "PXI_TOOLBAR_ENABLED",
449       "identity.fxaccounts.toolbar.pxiToolbarEnabled"
450     );
451   },
453   maybeUpdateUIState() {
454     // Update the UI.
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);
461       }
462     }
463   },
465   init() {
466     if (this._initialized) {
467       return;
468     }
470     this._definePrefGetters();
472     if (!this.FXA_ENABLED) {
473       this.onFxaDisabled();
474       return;
475     }
477     MozXULElement.insertFTLIfNeeded("browser/sync.ftl");
479     // Label for the sync buttons.
480     const appMenuLabel = PanelMultiView.getViewNode(
481       document,
482       "appMenu-fxa-label2"
483     );
484     if (!appMenuLabel) {
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.
487       return;
488     }
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(
497       document,
498       "PanelUI-remotetabs-setupsync"
499     ).hidden = false;
501     const appMenuHeaderTitle = PanelMultiView.getViewNode(
502       document,
503       "appMenu-header-title"
504     );
505     const appMenuHeaderDescription = PanelMultiView.getViewNode(
506       document,
507       "appMenu-header-description"
508     );
509     const appMenuHeaderText = PanelMultiView.getViewNode(
510       document,
511       "appMenu-fxa-text"
512     );
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",
521     ]);
522     appMenuHeaderDescription.value = headerDesc;
523     appMenuHeaderText.textContent = headerText;
525     for (let topic of this._obs) {
526       Services.obs.addObserver(this, topic, true);
527     }
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();
542     }
544     this._initialized = true;
545   },
547   uninit() {
548     if (!this._initialized) {
549       return;
550     }
552     for (let topic of this._obs) {
553       Services.obs.removeObserver(this, topic);
554     }
556     this._initialized = false;
557   },
559   handleEvent(event) {
560     switch (event.type) {
561       case "ViewShowing": {
562         this.onFxAPanelViewShowing(event.target);
563         break;
564       }
565       case "ViewHiding": {
566         this.onFxAPanelViewHiding(event.target);
567       }
568     }
569   },
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"
577     );
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(
583       document,
584       "PanelUI-fxa-menu-sync-prefs-button"
585     );
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(
591       document,
592       "PanelUI-fxa-menu-account-signout-button"
593     );
594     signOutButtonEl.hidden = !this.isSignedIn;
596     panelview.syncedTabsPanelList = new SyncedTabsPanelList(
597       panelview,
598       PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-deck"),
599       PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-tabslist"),
600       PanelMultiView.getViewNode(document, "PanelUI-remote-tabs-separator")
601     );
602   },
604   onFxAPanelViewHiding(panelview) {
605     panelview.syncedTabsPanelList.destroy();
606     panelview.syncedTabsPanelList = null;
607   },
609   observe(subject, topic, data) {
610     if (!this._initialized) {
611       console.error("browser-sync observer called after unload: ", topic);
612       return;
613     }
614     switch (topic) {
615       case UIState.ON_UPDATE:
616         const state = UIState.get();
617         this.updateAllUI(state);
618         break;
619       case "quit-application":
620         // Stop the animation timer on shutdown, since we can't update the UI
621         // after this.
622         clearTimeout(this._syncAnimationTimer);
623         break;
624       case "weave:engine:sync:finish":
625         if (data != "clients") {
626           return;
627         }
628         this.onClientsSynced();
629         this.updateFxAPanel(UIState.get());
630         break;
631     }
632   },
634   updateAllUI(state) {
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();
643   },
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");
654       return;
655     }
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.");
662         }
663       }
664     }
665   },
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
671   // call this.
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");
676       return false;
677     }
678     try {
679       // Do the actual refresh telling it to avoid the "flooding" protections.
680       await fxAccounts.device.refreshDeviceList({ ignoreCached: true });
681       return true;
682     } catch (e) {
683       this.log.error("Refreshing device list failed.", e);
684       return false;
685     }
686   },
688   updateSendToDeviceTitle() {
689     const tabCount = gBrowser.selectedTab.multiselected
690       ? gBrowser.selectedTabs.length
691       : 1;
692     document.l10n.setArgs(
693       PanelMultiView.getViewNode(document, "PanelUI-fxa-menu-sendtab-button"),
694       { tabCount }
695     );
696   },
698   showSendToDeviceView(anchor) {
699     PanelUI.showSubView("PanelUI-sendTabToDevice", anchor);
700     let panelViewNode = document.getElementById("PanelUI-sendTabToDevice");
701     this._populateSendTabToDevicesView(panelViewNode);
702   },
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);
708       return;
709     }
711     const targets = this.sendTabConfiguredAndLoading
712       ? []
713       : this.getSendTabTargets();
714     if (!targets.length) {
715       PanelUI.showSubView("PanelUI-fxa-menu-sendtab-no-devices", anchor);
716       return;
717     }
719     this.showSendToDeviceView(anchor);
720     this.emitFxaToolbarTelemetry("send_tab", anchor);
721   },
723   showRemoteTabsFromFxaMenu(panel) {
724     PanelUI.showSubView("PanelUI-remotetabs", panel);
725     this.emitFxaToolbarTelemetry("sync_tabs", panel);
726   },
728   showSidebarFromFxaMenu(panel) {
729     SidebarUI.toggle("viewTabsSidebar");
730     this.emitFxaToolbarTelemetry("sync_tabs_sidebar", panel);
731   },
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
742     // changes.
743     this.populateSendTabToDevicesMenu(
744       bodyNode,
745       uri,
746       title,
747       multiselected,
748       (clientId, name, clientType, lastModified) => {
749         if (!name) {
750           return document.createXULElement("toolbarseparator");
751         }
752         let item = document.createXULElement("toolbarbutton");
753         item.setAttribute("wrap", true);
754         item.setAttribute("align", "start");
755         item.classList.add("sendToDevice-device", "subviewbutton");
756         if (clientId) {
757           item.classList.add("subviewbutton-iconic");
758           if (lastModified) {
759             let lastSyncDate = gSync.formatLastSyncDate(lastModified);
760             if (lastSyncDate) {
761               item.setAttribute(
762                 "tooltiptext",
763                 this.fluentStrings.formatValueSync("appmenu-fxa-last-sync", {
764                   time: lastSyncDate,
765                 })
766               );
767             }
768           }
769         }
771         item.addEventListener("command", () => {
772           if (panelNode) {
773             PanelMultiView.hidePopup(panelNode);
774           }
775         });
776         return item;
777       },
778       true
779     );
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");
787     }
788     if (reloadDevices) {
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);
804         }
805       });
806     }
807   },
809   toggleAccountPanel(
810     anchor = document.getElementById("fxa-toolbar-menu-button"),
811     aEvent
812   ) {
813     // Don't show the panel if the window is in customization mode.
814     if (document.documentElement.hasAttribute("customizing")) {
815       return;
816     }
818     if (
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)
823     ) {
824       return;
825     }
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");
846         PanelUI.hide();
847       } else {
848         let panel =
849           anchor.id == "appMenu-fxa-label2"
850             ? PanelMultiView.getViewNode(document, "PanelUI-fxa")
851             : undefined;
852         this.openFxAEmailFirstPageFromFxaMenu(panel);
853         PanelUI.hide();
854       }
855       return;
856     }
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();
861     }
863     if (!gFxaToolbarAccessed) {
864       Services.prefs.setBoolPref("identity.fxaccounts.toolbar.accessed", true);
865     }
867     this.enableSendTabIfValidTab();
869     if (!this.getSendTabTargets().length) {
870       PanelMultiView.getViewNode(
871         document,
872         "PanelUI-fxa-menu-sendtab-button"
873       ).hidden = true;
874     }
876     if (anchor.getAttribute("open") == "true") {
877       PanelUI.hide();
878     } else {
879       this.emitFxaToolbarTelemetry("toolbar_icon", anchor);
880       PanelUI.showSubView("PanelUI-fxa", anchor, aEvent);
881     }
882   },
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(
895       document,
896       "fxa-menu-header-title"
897     );
898     const menuHeaderDescriptionEl = PanelMultiView.getViewNode(
899       document,
900       "fxa-menu-header-description"
901     );
903     const cadButtonEl = PanelMultiView.getViewNode(
904       document,
905       "PanelUI-fxa-menu-connect-device-button"
906     );
908     const syncSetupButtonEl = PanelMultiView.getViewNode(
909       document,
910       "PanelUI-fxa-menu-setup-sync-button"
911     );
913     const syncNowButtonEl = PanelMultiView.getViewNode(
914       document,
915       "PanelUI-fxa-menu-syncnow-button"
916     );
918     const fxaMenuAccountButtonEl = PanelMultiView.getViewNode(
919       document,
920       "fxa-manage-account-button"
921     );
923     const signedInContainer = PanelMultiView.getViewNode(
924       document,
925       "PanelUI-signedin-panel"
926     );
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"
943       );
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
946       // the value
947       if (this.PXI_TOOLBAR_ENABLED) {
948         headerDescription = "";
949       }
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();
965         img.onload = () => {
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);
969         };
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");
974         };
975         img.src = state.avatarURL;
976         signedInContainer.hidden = false;
977         menuHeaderDescriptionEl.hidden = false;
978       } else {
979         mainWindowEl.style.removeProperty("--avatar-image-url");
980       }
982       cadButtonEl.removeAttribute("disabled");
984       if (state.syncEnabled) {
985         syncNowButtonEl.removeAttribute("hidden");
986         syncSetupButtonEl.hidden = true;
987       }
989       headerTitleL10nId = "appmenuitem-fxa-manage-account";
990       headerDescription = state.displayName || state.email;
991     } else {
992       headerDescription = this.fluentStrings.formatValueSync(
993         "fxa-menu-turn-on-sync-default"
994       );
995     }
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");
1008   },
1010   enableSendTabIfValidTab() {
1011     // All tabs selected must be sendable for the Send Tab button to be enabled
1012     // on the FxA menu.
1013     let canSendAllURIs = gBrowser.selectedTabs.every(
1014       t => !!BrowserUtils.getShareableURL(t.linkedBrowser.currentURI)
1015     );
1017     PanelMultiView.getViewNode(
1018       document,
1019       "PanelUI-fxa-menu-sendtab-button"
1020     ).hidden = !canSendAllURIs;
1021   },
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",
1030       };
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";
1037       }
1039       Services.telemetry.recordEvent(
1040         eventName,
1041         "click",
1042         type,
1043         null,
1044         extraOptions
1045       );
1046     }
1047   },
1049   isPanelInsideAppMenu(panel = undefined) {
1050     const appMenuPanel = document.getElementById("appMenu-popup");
1051     if (panel && appMenuPanel.contains(panel)) {
1052       return true;
1053     }
1054     return false;
1055   },
1057   updatePanelPopup({ email, displayName, status }) {
1058     const appMenuStatus = PanelMultiView.getViewNode(
1059       document,
1060       "appMenu-fxa-status2"
1061     );
1062     const appMenuLabel = PanelMultiView.getViewNode(
1063       document,
1064       "appMenu-fxa-label2"
1065     );
1066     const appMenuHeaderText = PanelMultiView.getViewNode(
1067       document,
1068       "appMenu-fxa-text"
1069     );
1070     const appMenuHeaderTitle = PanelMultiView.getViewNode(
1071       document,
1072       "appMenu-header-title"
1073     );
1074     const appMenuHeaderDescription = PanelMultiView.getViewNode(
1075       document,
1076       "appMenu-header-description"
1077     );
1078     const fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa");
1080     let defaultLabel = this.fluentStrings.formatValueSync(
1081       "appmenu-fxa-signed-in-label"
1082     );
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;
1094       return;
1095     }
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" },
1109         ]);
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(
1119         "aria-labelledby",
1120         `${appMenuHeaderTitle.id},${appMenuHeaderDescription.id}`
1121       );
1122       return;
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" },
1128         ]);
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(
1138         "aria-labelledby",
1139         `${appMenuHeaderTitle.id},${appMenuHeaderDescription.id}`
1140       );
1141       return;
1142     }
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(
1150       "title",
1151       this.fluentStrings.formatValueSync("appmenu-account-header")
1152     );
1153     appMenuStatus.removeAttribute("tooltiptext");
1154   },
1156   updateState(state) {
1157     for (let [shown, menuId, boxId] of [
1158       [
1159         state.status == UIState.STATUS_NOT_CONFIGURED,
1160         "sync-setup",
1161         "PanelUI-remotetabs-setupsync",
1162       ],
1163       [
1164         state.status == UIState.STATUS_SIGNED_IN && !state.syncEnabled,
1165         "sync-enable",
1166         "PanelUI-remotetabs-syncdisabled",
1167       ],
1168       [
1169         state.status == UIState.STATUS_LOGIN_FAILED,
1170         "sync-reauthitem",
1171         "PanelUI-remotetabs-reauthsync",
1172       ],
1173       [
1174         state.status == UIState.STATUS_NOT_VERIFIED,
1175         "sync-unverifieditem",
1176         "PanelUI-remotetabs-unverified",
1177       ],
1178       [
1179         state.status == UIState.STATUS_SIGNED_IN && state.syncEnabled,
1180         "sync-syncnowitem",
1181         "PanelUI-remotetabs-main",
1182       ],
1183     ]) {
1184       document.getElementById(menuId).hidden = PanelMultiView.getViewNode(
1185         document,
1186         boxId
1187       ).hidden = !shown;
1188     }
1189   },
1191   updateSyncStatus(state) {
1192     let syncNow =
1193       document.querySelector(".syncNowBtn") ||
1194       document
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();
1201     }
1202   },
1204   async openSignInAgainPage(entryPoint) {
1205     if (!(await FxAccounts.canConnectAccount())) {
1206       return;
1207     }
1208     const url = await FxAccounts.config.promiseForceSigninURI(entryPoint);
1209     switchToTabHavingURI(url, true, {
1210       replaceQueryString: true,
1211       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1212     });
1213   },
1215   async openDevicesManagementPage(entryPoint) {
1216     let url = await FxAccounts.config.promiseManageDevicesURI(entryPoint);
1217     switchToTabHavingURI(url, true, {
1218       replaceQueryString: true,
1219       triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
1220     });
1221   },
1223   async openConnectAnotherDevice(entryPoint) {
1224     const url = await FxAccounts.config.promiseConnectDeviceURI(entryPoint);
1225     openTrustedLinkIn(url, "tab");
1226   },
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";
1233     }
1234     this.openConnectAnotherDevice(entryPoint);
1235   },
1237   openSendToDevicePromo() {
1238     const url = Services.urlFormatter.formatURLPref(
1239       "identity.sendtabpromo.url"
1240     );
1241     switchToTabHavingURI(url, true, { replaceQueryString: true });
1242   },
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();
1250     switch (status) {
1251       case UIState.STATUS_NOT_CONFIGURED:
1252         this.openFxAEmailFirstPageFromFxaMenu(panel);
1253         break;
1254       case UIState.STATUS_LOGIN_FAILED:
1255         this.openPrefsFromFxaMenu("sync_settings", panel);
1256         break;
1257       case UIState.STATUS_NOT_VERIFIED:
1258         this.openFxAEmailFirstPage("fxa_app_menu_reverify");
1259         break;
1260       case UIState.STATUS_SIGNED_IN:
1261         this.openFxAManagePageFromFxaMenu(panel);
1262     }
1263   },
1265   async openFxAEmailFirstPage(entryPoint, extraParams = {}) {
1266     if (!(await FxAccounts.canConnectAccount())) {
1267       return;
1268     }
1269     const url = await FxAccounts.config.promiseConnectAccountURI(
1270       entryPoint,
1271       extraParams
1272     );
1273     switchToTabHavingURI(url, true, { replaceQueryString: true });
1274   },
1276   async openFxAEmailFirstPageFromFxaMenu(panel = undefined, extraParams = {}) {
1277     this.emitFxaToolbarTelemetry("login", panel);
1278     let entryPoint = "fxa_discoverability_native";
1279     if (panel) {
1280       entryPoint = "fxa_toolbar_button";
1281     }
1282     this.openFxAEmailFirstPage(entryPoint, extraParams);
1283   },
1285   async openFxAManagePage(entryPoint) {
1286     const url = await FxAccounts.config.promiseManageURI(entryPoint);
1287     switchToTabHavingURI(url, true, { replaceQueryString: true });
1288   },
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";
1295     }
1296     this.openFxAManagePage(entryPoint);
1297   },
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);
1305       } else {
1306         this.log.error(`Target ${target.id} unsuitable for send tab.`);
1307       }
1308     }
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
1315     );
1316     if (!cryptoSDR.isLoggedIn) {
1317       if (cryptoSDR.uiBusy) {
1318         this.log.info("Master password UI is busy - not sending the tabs");
1319         return false;
1320       }
1321       try {
1322         cryptoSDR.encrypt("bacon"); // forces the mp prompt.
1323       } catch (e) {
1324         this.log.info(
1325           "Master password remains unlocked - not sending the tabs"
1326         );
1327         return false;
1328       }
1329     }
1330     let numFailed = 0;
1331     if (fxaCommandsDevices.length) {
1332       this.log.info(
1333         `Sending a tab to ${fxaCommandsDevices
1334           .map(d => d.id)
1335           .join(", ")} using FxA commands.`
1336       );
1337       const report = await fxAccounts.commands.sendTab.send(
1338         fxaCommandsDevices,
1339         { url, title }
1340       );
1341       for (let { device, error } of report.failed) {
1342         this.log.error(
1343           `Failed to send a tab with FxA commands for ${device.id}.`,
1344           error
1345         );
1346         numFailed++;
1347       }
1348     }
1349     return numFailed < targets.length; // Good enough.
1350   },
1352   populateSendTabToDevicesMenu(
1353     devicesPopup,
1354     uri,
1355     title,
1356     multiselected,
1357     createDeviceNodeFn,
1358     isFxaMenu = false
1359   ) {
1360     uri = BrowserUtils.getShareableURL(uri);
1361     if (!uri) {
1362       // log an error as everyone should have already checked this.
1363       this.log.error("Ignoring request to share a non-sharable URL");
1364       return;
1365     }
1366     if (!createDeviceNodeFn) {
1367       createDeviceNodeFn = (targetId, name) => {
1368         let eltName = name ? "menuitem" : "menuseparator";
1369         return document.createXULElement(eltName);
1370       };
1371     }
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")) {
1377         child.remove();
1378       }
1379     }
1381     if (gSync.sendTabConfiguredAndLoading) {
1382       // We can only be in this case in the page action menu.
1383       return;
1384     }
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(
1393           targets,
1394           fragment,
1395           createDeviceNodeFn,
1396           uri.spec,
1397           title,
1398           multiselected,
1399           isFxaMenu
1400         );
1401       } else {
1402         this._appendSendTabSingleDevice(fragment, createDeviceNodeFn);
1403       }
1404     } else if (
1405       state.status == UIState.STATUS_NOT_VERIFIED ||
1406       state.status == UIState.STATUS_LOGIN_FAILED
1407     ) {
1408       this._appendSendTabVerify(fragment, createDeviceNodeFn);
1409     } else {
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.
1414       throw new Error(
1415         "Called populateSendTabToDevicesMenu when in STATUS_NOT_CONFIGURED " +
1416           "state."
1417       );
1418     }
1420     devicesPopup.appendChild(fragment);
1421   },
1423   _appendSendTabDeviceList(
1424     targets,
1425     fragment,
1426     createDeviceNodeFn,
1427     url,
1428     title,
1429     multiselected,
1430     isFxaMenu = false
1431   ) {
1432     let tabsToSend = multiselected
1433       ? gBrowser.selectedTabs.map(t => {
1434           return {
1435             url: t.linkedBrowser.currentURI.spec,
1436             title: t.linkedBrowser.contentTitle,
1437           };
1438         })
1439       : [{ url, title }];
1441     const send = to => {
1442       Promise.all(
1443         tabsToSend.map(t =>
1444           // sendTabToDevice does not reject.
1445           this.sendTabToDevice(t.url, to, t.title)
1446         )
1447       ).then(results => {
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");
1453           let anchorNode =
1454             (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");
1461         }
1462         fxAccounts.flushLogFile();
1463       });
1464     };
1465     const onSendAllCommand = () => {
1466       send(targets);
1467     };
1468     const onTargetDeviceCommand = event => {
1469       const targetId = event.target.getAttribute("clientId");
1470       const target = targets.find(t => t.id == targetId);
1471       send([target]);
1472     };
1474     function addTargetDevice(targetId, name, targetType, lastModified) {
1475       const targetDevice = createDeviceNodeFn(
1476         targetId,
1477         name,
1478         targetType,
1479         lastModified
1480       );
1481       targetDevice.addEventListener(
1482         "command",
1483         targetId ? onTargetDeviceCommand : onSendAllCommand,
1484         true
1485       );
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);
1491     }
1493     for (let target of targets) {
1494       let type, lastModified;
1495       if (target.clientRecord) {
1496         type = Weave.Service.clientsEngine.getClientType(
1497           target.clientRecord.id
1498         );
1499         lastModified = new Date(target.clientRecord.serverLastModified * 1000);
1500       } else {
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)
1505           : null;
1506       }
1507       addTargetDevice(target.id, target.name, type, lastModified);
1508     }
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(
1517           isFxaMenu
1518             ? ["account-send-to-all-devices", "account-manage-devices"]
1519             : [
1520                 "account-send-to-all-devices-titlecase",
1521                 "account-manage-devices-titlecase",
1522               ]
1523         );
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(
1530         null,
1531         manageDevicesLabel,
1532         null,
1533         null
1534       );
1535       targetDevice.addEventListener(
1536         "command",
1537         () => gSync.openDevicesManagementPage("sendtab"),
1538         true
1539       );
1540       targetDevice.classList.add("sync-menuitem", "sendtab-target");
1541       targetDevice.setAttribute("label", manageDevicesLabel);
1542       fragment.appendChild(targetDevice);
1543     }
1544   },
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",
1552       ]);
1553     const actions = [
1554       {
1555         label: connectDevice,
1556         command: () => this.openConnectAnotherDevice("sendtab"),
1557       },
1558       { label: learnMore, command: () => this.openSendToDevicePromo() },
1559     ];
1560     this._appendSendTabInfoItems(
1561       fragment,
1562       createDeviceNodeFn,
1563       noDevices,
1564       actions
1565     );
1566   },
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",
1572     ]);
1573     const actions = [
1574       { label: verifyAccount, command: () => this.openPrefs("sendtab") },
1575     ];
1576     this._appendSendTabInfoItems(
1577       fragment,
1578       createDeviceNodeFn,
1579       notVerified,
1580       actions
1581     );
1582   },
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);
1601     }
1602   },
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.
1608     this.init();
1610     if (!this.FXA_ENABLED) {
1611       // These items are hidden in onFxaDisabled(). No need to do anything.
1612       return;
1613     }
1614     let hasASendableURI = false;
1615     for (let tab of aTargetTab.multiselected
1616       ? gBrowser.selectedTabs
1617       : [aTargetTab]) {
1618       if (BrowserUtils.getShareableURL(tab.linkedBrowser.currentURI)) {
1619         hasASendableURI = true;
1620         break;
1621       }
1622     }
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;
1631     } else {
1632       let tabCount = aTargetTab.multiselected
1633         ? gBrowser.multiSelectedTabsCount
1634         : 1;
1635       sendTabsToDevice.setAttribute(
1636         "data-l10n-args",
1637         JSON.stringify({ tabCount })
1638       );
1639       sendTabsToDevice.hidden = false;
1640     }
1641   },
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.
1647       return false;
1648     }
1649     // showSendLink and showSendPage are mutually exclusive
1650     const showSendLink =
1651       contextMenu.onSaveableLink || contextMenu.onPlainTextLink;
1652     const showSendPage =
1653       !showSendLink &&
1654       !(
1655         contextMenu.isContentSelected ||
1656         contextMenu.onImage ||
1657         contextMenu.onCanvas ||
1658         contextMenu.onVideo ||
1659         contextMenu.onAudio ||
1660         contextMenu.onLink ||
1661         contextMenu.onTextInput
1662       );
1664     const targetURI = showSendLink
1665       ? contextMenu.getLinkURI()
1666       : contextMenu.browser.currentURI;
1667     const enabled =
1668       !this.sendTabConfiguredAndLoading &&
1669       BrowserUtils.getShareableURL(targetURI);
1670     const hideItems = this.shouldHideSendContextMenuItems(enabled);
1672     contextMenu.showItem(
1673       "context-sendpagetodevice",
1674       !hideItems && showSendPage
1675     );
1676     contextMenu.showItem(
1677       "context-sendlinktodevice",
1678       !hideItems && showSendLink
1679     );
1681     if (!showSendLink && !showSendPage) {
1682       return false;
1683     }
1685     contextMenu.setItemAttr(
1686       showSendPage ? "context-sendpagetodevice" : "context-sendlinktodevice",
1687       "disabled",
1688       !enabled || null
1689     );
1690     // return true if context menu items are visible
1691     return !hideItems && (showSendPage || showSendLink);
1692   },
1694   // Functions called by observers
1695   onActivityStart() {
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);
1703     });
1705     document.querySelectorAll(".syncNowBtn").forEach(el => {
1706       el.setAttribute("syncstatus", "active");
1707     });
1709     document
1710       .getElementById("appMenu-viewCache")
1711       .content.querySelectorAll(".syncNowBtn")
1712       .forEach(el => {
1713         el.setAttribute("syncstatus", "active");
1714       });
1715   },
1717   _onActivityStop() {
1718     this._isCurrentlySyncing = false;
1719     if (!gBrowser) {
1720       return;
1721     }
1723     document.querySelectorAll(".syncnow-label").forEach(el => {
1724       let l10nId = el.getAttribute("sync-now-data-l10n-id");
1725       document.l10n.setAttributes(el, l10nId);
1726     });
1728     document.querySelectorAll(".syncNowBtn").forEach(el => {
1729       el.removeAttribute("syncstatus");
1730     });
1732     document
1733       .getElementById("appMenu-viewCache")
1734       .content.querySelectorAll(".syncNowBtn")
1735       .forEach(el => {
1736         el.removeAttribute("syncstatus");
1737       });
1739     Services.obs.notifyObservers(null, "test:browser-sync:activity-stop");
1740   },
1742   onActivityStop() {
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(),
1751         animationTime
1752       );
1753     } else {
1754       this._onActivityStop();
1755     }
1756   },
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;
1764       if (confirm) {
1765         let options = await this._confirmFxaAndSyncDisconnect();
1766         if (!options.userConfirmedDisconnect) {
1767           return false;
1768         }
1769         deleteLocalData = options.deleteLocalData;
1770       }
1771       return this._disconnectFxaAndSync(deleteLocalData);
1772     }
1774     if (confirm && !(await this._confirmSyncDisconnect())) {
1775       return false;
1776     }
1777     return this._disconnectSync();
1778   },
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() {
1783     let options = {
1784       userConfirmedDisconnect: false,
1785       deleteLocalData: false,
1786     };
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" },
1793     ]);
1795     const flags =
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) {
1800       checkbox = null;
1801     }
1803     const result = await Services.prompt.asyncConfirmEx(
1804       window.browsingContext,
1805       Services.prompt.MODAL_TYPE_INTERNAL_WINDOW,
1806       title,
1807       body,
1808       flags,
1809       button,
1810       null,
1811       null,
1812       checkbox,
1813       false
1814     );
1815     const propBag = result.QueryInterface(Ci.nsIPropertyBag2);
1816     options.userConfirmedDisconnect = propBag.get("buttonNumClicked") == 0;
1817     options.deleteLocalData = propBag.get("checked");
1819     return options;
1820   },
1822   async _disconnectFxaAndSync(deleteLocalData) {
1823     const { SyncDisconnect } = ChromeUtils.importESModule(
1824       "resource://services-sync/SyncDisconnect.sys.mjs"
1825     );
1826     // Record telemetry.
1827     await fxAccounts.telemetry.recordDisconnection(null, "ui");
1829     await SyncDisconnect.disconnect(deleteLocalData).catch(e => {
1830       console.error("Failed to disconnect.", e);
1831     });
1833     return true;
1834   },
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" },
1843     ]);
1845     const flags =
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(
1851       window,
1852       title,
1853       body,
1854       flags,
1855       button,
1856       null,
1857       null,
1858       null,
1859       {}
1860     );
1861     return buttonPressed == 0;
1862   },
1864   async _disconnectSync() {
1865     await fxAccounts.telemetry.recordDisconnection("sync", "ui");
1867     await Weave.Service.promiseInitialized;
1868     await Weave.Service.startOver();
1870     return true;
1871   },
1873   // doSync forces a sync - it *does not* return a promise as it is called
1874   // via the various UI components.
1875   doSync() {
1876     if (!UIState.isReady()) {
1877       return;
1878     }
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);
1890         });
1891         Weave.Service.sync();
1892       });
1893     }
1894   },
1896   doSyncFromFxaMenu(panel) {
1897     this.doSync();
1898     this.emitFxaToolbarTelemetry("sync_now", panel);
1899   },
1901   openPrefs(entryPoint = "syncbutton", origin = undefined) {
1902     window.openPreferences("paneSync", {
1903       origin,
1904       urlParams: { entrypoint: entryPoint },
1905     });
1906   },
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";
1913     }
1914     this.openPrefs(entryPoint);
1915   },
1917   openPrefsFromFxaButton(type, panel) {
1918     let entryPoint = "fxa_toolbar_button_sync";
1919     this.emitFxaToolbarTelemetry(type, panel);
1920     this.openPrefs(entryPoint);
1921   },
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);
1933       }, console.error);
1934     } else {
1935       if (
1936         !anchor?.checkVisibility({ checkVisibilityCSS: true, flush: false })
1937       ) {
1938         anchor = document.getElementById("PanelUI-menu-button");
1939       }
1940       // It is placed somewhere else - just try and show it.
1941       PanelUI.showSubView("PanelUI-remotetabs", anchor);
1942     }
1943   },
1945   refreshSyncButtonsTooltip() {
1946     const state = UIState.get();
1947     this.updateSyncButtonsTooltip(state);
1948   },
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
1953      (re-)configured.
1954   */
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 };
1963         break;
1964       case UIState.STATUS_LOGIN_FAILED:
1965         // "need to reconnect/re-enter your password"
1966         l10nId = "account-reconnect";
1967         l10nArgs = { email: state.email };
1968         break;
1969       case UIState.STATUS_NOT_CONFIGURED:
1970         // Button is not shown in this state
1971         break;
1972       default: {
1973         // Sync appears configured - format the "last synced at" time.
1974         let lastSyncDate = this.formatLastSyncDate(state.lastSync);
1975         if (lastSyncDate) {
1976           l10nId = "appmenu-fxa-last-sync";
1977           l10nArgs = { time: lastSyncDate };
1978         }
1979       }
1980     }
1981     const tooltiptext = l10nId
1982       ? this.fluentStrings.formatValueSync(l10nId, l10nArgs)
1983       : null;
1985     let syncNowBtns = [
1986       "PanelUI-remotetabs-syncnow",
1987       "PanelUI-fxa-menu-syncnow-button",
1988     ];
1989     syncNowBtns.forEach(id => {
1990       let el = PanelMultiView.getViewNode(document, id);
1991       if (tooltiptext) {
1992         el.setAttribute("tooltiptext", tooltiptext);
1993       } else {
1994         el.removeAttribute("tooltiptext");
1995       }
1996     });
1997   },
1999   get relativeTimeFormat() {
2000     delete this.relativeTimeFormat;
2001     return (this.relativeTimeFormat = new Services.intl.RelativeTimeFormat(
2002       undefined,
2003       { style: "long" }
2004     ));
2005   },
2007   formatLastSyncDate(date) {
2008     if (!date) {
2009       // Date can be null before the first sync!
2010       return null;
2011     }
2012     try {
2013       let adjustedDate = new Date(Date.now() - 1000);
2014       let relativeDateStr = this.relativeTimeFormat.formatBestUnit(
2015         date < adjustedDate ? date : adjustedDate
2016       );
2017       return relativeDateStr;
2018     } catch (ex) {
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);
2022       return null;
2023     }
2024   },
2026   onClientsSynced() {
2027     // Note that this element is only shown if Sync is enabled.
2028     let element = PanelMultiView.getViewNode(
2029       document,
2030       "PanelUI-remotetabs-main"
2031     );
2032     if (element) {
2033       if (Weave.Service.clientsEngine.stats.numClients > 1) {
2034         element.setAttribute("devices-status", "multi");
2035       } else {
2036         element.setAttribute("devices-status", "single");
2037       }
2038     }
2039   },
2041   onFxaDisabled() {
2042     document.documentElement.setAttribute("fxadisabled", true);
2044     const toHide = [...document.querySelectorAll(".sync-ui-item")];
2045     for (const item of toHide) {
2046       item.hidden = true;
2047     }
2048   },
2050   // This should only be shown if we have enabled the pxiPanel via
2051   // an experiment or explicitly through prefs
2052   updateCTAPanel() {
2053     const mainPanelEl = PanelMultiView.getViewNode(
2054       document,
2055       "PanelUI-fxa-cta-menu"
2056     );
2058     const syncCtaEl = PanelMultiView.getViewNode(
2059       document,
2060       "PanelUI-fxa-menu-sync-button"
2061     );
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;
2067       return;
2068     }
2070     // If we're already signed in an syncing, we shouldn't show the sync CTA
2071     syncCtaEl.hidden = this.isSignedIn;
2073     // Monitor checks
2074     let monitorPanelEl = PanelMultiView.getViewNode(
2075       document,
2076       "PanelUI-fxa-menu-monitor-button"
2077     );
2078     let monitorEnabled = Services.prefs.getBoolPref(
2079       "identity.fxaccounts.toolbar.pxiToolbarEnabled.monitorEnabled",
2080       false
2081     );
2082     monitorPanelEl.hidden = !monitorEnabled;
2084     // Relay checks
2085     let relayPanelEl = PanelMultiView.getViewNode(
2086       document,
2087       "PanelUI-fxa-menu-relay-button"
2088     );
2089     let relayEnabled =
2090       BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.RELAY) &&
2091       Services.prefs.getBoolPref(
2092         "identity.fxaccounts.toolbar.pxiToolbarEnabled.relayEnabled",
2093         false
2094       );
2095     relayPanelEl.hidden = !relayEnabled;
2097     // VPN checks
2098     let VpnPanelEl = PanelMultiView.getViewNode(
2099       document,
2100       "PanelUI-fxa-menu-vpn-button"
2101     );
2102     let vpnEnabled =
2103       BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.VPN) &&
2104       Services.prefs.getBoolPref(
2105         "identity.fxaccounts.toolbar.pxiToolbarEnabled.vpnEnabled",
2106         false
2107       );
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;
2114   },
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")
2121     );
2122   },
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")
2130     );
2131   },
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/")
2139     );
2140   },
2142   // A generic opening based on
2143   async openCtaLink(clientId, defaultUrl, signedInUrl) {
2144     const params = {
2145       utm_medium: "firefox-desktop",
2146       utm_source: "toolbar",
2147       utm_campaign: "discovery",
2148     };
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);
2156       PanelUI.hide();
2157       return;
2158     }
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");
2170     this.openLink(url);
2171     PanelUI.hide();
2172   },
2174   openLink(url) {
2175     switchToTabHavingURI(url, true, { replaceQueryString: true });
2176   },
2178   QueryInterface: ChromeUtils.generateQI([
2179     "nsIObserver",
2180     "nsISupportsWeakReference",
2181   ]),