browser-sync.js (90803B)
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/. */ 4 5 const { 6 FX_MONITOR_OAUTH_CLIENT_ID, 7 FX_RELAY_OAUTH_CLIENT_ID, 8 SCOPE_APP_SYNC, 9 VPN_OAUTH_CLIENT_ID, 10 } = ChromeUtils.importESModule( 11 "resource://gre/modules/FxAccountsCommon.sys.mjs" 12 ); 13 14 const { TRUSTED_FAVICON_SCHEMES, getMozRemoteImageURL } = 15 ChromeUtils.importESModule("moz-src:///browser/modules/FaviconUtils.sys.mjs"); 16 17 const { UIState } = ChromeUtils.importESModule( 18 "resource://services-sync/UIState.sys.mjs" 19 ); 20 21 ChromeUtils.defineESModuleGetters(this, { 22 ASRouter: "resource:///modules/asrouter/ASRouter.sys.mjs", 23 EnsureFxAccountsWebChannel: 24 "resource://gre/modules/FxAccountsWebChannel.sys.mjs", 25 26 ExperimentAPI: "resource://nimbus/ExperimentAPI.sys.mjs", 27 FxAccounts: "resource://gre/modules/FxAccounts.sys.mjs", 28 MenuMessage: "resource:///modules/asrouter/MenuMessage.sys.mjs", 29 SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", 30 SyncedTabsManagement: "resource://services-sync/SyncedTabs.sys.mjs", 31 Weave: "resource://services-sync/main.sys.mjs", 32 }); 33 34 const MIN_STATUS_ANIMATION_DURATION = 1600; 35 36 this.SyncedTabsPanelList = class SyncedTabsPanelList { 37 static sRemoteTabsDeckIndices = { 38 DECKINDEX_TABS: 0, 39 DECKINDEX_FETCHING: 1, 40 DECKINDEX_TABSDISABLED: 2, 41 DECKINDEX_NOCLIENTS: 3, 42 }; 43 44 static sRemoteTabsPerPage = 25; 45 static sRemoteTabsNextPageMinTabs = 5; 46 47 constructor(panelview, deck, tabsList, separator) { 48 this.QueryInterface = ChromeUtils.generateQI([ 49 "nsIObserver", 50 "nsISupportsWeakReference", 51 ]); 52 53 Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED, true); 54 this.deck = deck; 55 this.tabsList = tabsList; 56 this.separator = separator; 57 this._showSyncedTabsPromise = Promise.resolve(); 58 59 this.createSyncedTabs(); 60 } 61 62 observe(subject, topic) { 63 if (topic == SyncedTabs.TOPIC_TABS_CHANGED) { 64 this._showSyncedTabs(); 65 } 66 } 67 68 createSyncedTabs() { 69 if (SyncedTabs.isConfiguredToSyncTabs) { 70 if (SyncedTabs.hasSyncedThisSession) { 71 this.deck.selectedIndex = 72 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABS; 73 } else { 74 // Sync hasn't synced tabs yet, so show the "fetching" panel. 75 this.deck.selectedIndex = 76 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_FETCHING; 77 } 78 // force a background sync. 79 SyncedTabs.syncTabs().catch(ex => { 80 console.error(ex); 81 }); 82 this.deck.toggleAttribute("syncingtabs", true); 83 // show the current list - it will be updated by our observer. 84 this._showSyncedTabs(); 85 if (this.separator) { 86 this.separator.hidden = false; 87 } 88 } else { 89 // not configured to sync tabs, so no point updating the list. 90 this.deck.selectedIndex = 91 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABSDISABLED; 92 this.deck.toggleAttribute("syncingtabs", false); 93 if (this.separator) { 94 this.separator.hidden = true; 95 } 96 } 97 } 98 99 // Update the synced tab list after any existing in-flight updates are complete. 100 _showSyncedTabs(paginationInfo) { 101 this._showSyncedTabsPromise = this._showSyncedTabsPromise.then( 102 () => { 103 return this.__showSyncedTabs(paginationInfo); 104 }, 105 e => { 106 console.error(e); 107 } 108 ); 109 } 110 111 // Return a new promise to update the tab list. 112 __showSyncedTabs(paginationInfo) { 113 if (!this.tabsList) { 114 // Closed between the previous `this._showSyncedTabsPromise` 115 // resolving and now. 116 return undefined; 117 } 118 return SyncedTabs.getTabClients() 119 .then(clients => { 120 let noTabs = !UIState.get().syncEnabled || !clients.length; 121 this.deck.toggleAttribute("syncingtabs", !noTabs); 122 if (this.separator) { 123 this.separator.hidden = noTabs; 124 } 125 126 // The view may have been hidden while the promise was resolving. 127 if (!this.tabsList) { 128 return; 129 } 130 if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) { 131 // the "fetching tabs" deck is being shown - let's leave it there. 132 // When that first sync completes we'll be notified and update. 133 return; 134 } 135 136 if (clients.length === 0) { 137 this.deck.selectedIndex = 138 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_NOCLIENTS; 139 return; 140 } 141 this.deck.selectedIndex = 142 SyncedTabsPanelList.sRemoteTabsDeckIndices.DECKINDEX_TABS; 143 this._clearSyncedTabList(); 144 SyncedTabs.sortTabClientsByLastUsed(clients); 145 let fragment = document.createDocumentFragment(); 146 147 let clientNumber = 0; 148 for (let client of clients) { 149 // add a menu separator for all clients other than the first. 150 if (fragment.lastElementChild) { 151 let separator = document.createXULElement("toolbarseparator"); 152 fragment.appendChild(separator); 153 } 154 // We add the client's elements to a container, and indicate which 155 // element labels it. 156 let labelId = `synced-tabs-client-${clientNumber++}`; 157 let container = document.createXULElement("vbox"); 158 container.classList.add("PanelUI-remotetabs-clientcontainer"); 159 container.setAttribute("role", "group"); 160 container.setAttribute("aria-labelledby", labelId); 161 let clientPaginationInfo = 162 paginationInfo && paginationInfo.clientId == client.id 163 ? paginationInfo 164 : { clientId: client.id }; 165 this._appendSyncClient( 166 client, 167 container, 168 labelId, 169 clientPaginationInfo 170 ); 171 fragment.appendChild(container); 172 } 173 this.tabsList.appendChild(fragment); 174 }) 175 .catch(err => { 176 console.error(err); 177 }) 178 .then(() => { 179 // an observer for tests. 180 Services.obs.notifyObservers( 181 null, 182 "synced-tabs-menu:test:tabs-updated" 183 ); 184 }); 185 } 186 187 _clearSyncedTabList() { 188 let list = this.tabsList; 189 while (list.lastChild) { 190 list.lastChild.remove(); 191 } 192 } 193 194 _createNoSyncedTabsElement(messageAttr, appendTo = null) { 195 if (!appendTo) { 196 appendTo = this.tabsList; 197 } 198 199 let messageLabel = document.createXULElement("label"); 200 document.l10n.setAttributes( 201 messageLabel, 202 this.tabsList.getAttribute(messageAttr) 203 ); 204 appendTo.appendChild(messageLabel); 205 return messageLabel; 206 } 207 208 _appendSyncClient(client, container, labelId, paginationInfo) { 209 let { maxTabs = SyncedTabsPanelList.sRemoteTabsPerPage } = paginationInfo; 210 // Create the element for the remote client. 211 let clientItem = document.createXULElement("label"); 212 clientItem.setAttribute("id", labelId); 213 clientItem.setAttribute("itemtype", "client"); 214 clientItem.setAttribute( 215 "tooltiptext", 216 gSync.fluentStrings.formatValueSync("appmenu-fxa-last-sync", { 217 time: gSync.formatLastSyncDate(new Date(client.lastModified)), 218 }) 219 ); 220 clientItem.textContent = client.name; 221 222 container.appendChild(clientItem); 223 224 if (!client.tabs.length) { 225 let label = this._createNoSyncedTabsElement( 226 "notabsforclientlabel", 227 container 228 ); 229 label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label"); 230 } else { 231 // We have the client obj but we need the FxA device obj so we use the clients 232 // engine to get us the FxA device 233 let device = 234 fxAccounts.device.recentDeviceList && 235 fxAccounts.device.recentDeviceList.find( 236 d => 237 d.id === Weave.Service.clientsEngine.getClientFxaDeviceId(client.id) 238 ); 239 let remoteTabCloseAvailable = 240 device && fxAccounts.commands.closeTab.isDeviceCompatible(device); 241 242 let tabs = client.tabs.filter(t => !t.inactive); 243 let hasInactive = tabs.length != client.tabs.length; 244 245 if (hasInactive) { 246 container.append(this._createShowInactiveTabsElement(client, device)); 247 } 248 // If this page isn't displaying all (regular, active) tabs, show a "Show More" button. 249 let hasNextPage = tabs.length > maxTabs; 250 let nextPageIsLastPage = 251 hasNextPage && 252 maxTabs + SyncedTabsPanelList.sRemoteTabsPerPage >= tabs.length; 253 if (nextPageIsLastPage) { 254 // When the user clicks "Show More", try to have at least sRemoteTabsNextPageMinTabs more tabs 255 // to display in order to avoid user frustration 256 maxTabs = Math.min( 257 tabs.length - SyncedTabsPanelList.sRemoteTabsNextPageMinTabs, 258 maxTabs 259 ); 260 } 261 if (hasNextPage) { 262 tabs = tabs.slice(0, maxTabs); 263 } 264 for (let [index, tab] of tabs.entries()) { 265 let tabEnt = this._createSyncedTabElement( 266 tab, 267 index, 268 device, 269 remoteTabCloseAvailable 270 ); 271 container.appendChild(tabEnt); 272 } 273 if (hasNextPage) { 274 let showAllEnt = this._createShowMoreSyncedTabsElement(paginationInfo); 275 container.appendChild(showAllEnt); 276 } 277 } 278 } 279 280 _createSyncedTabElement(tabInfo, index, device, canCloseTabs) { 281 let tabContainer = document.createXULElement("hbox"); 282 tabContainer.setAttribute( 283 "class", 284 "PanelUI-tabitem-container all-tabs-item" 285 ); 286 287 let item = document.createXULElement("toolbarbutton"); 288 let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url; 289 item.setAttribute("itemtype", "tab"); 290 item.classList.add( 291 "all-tabs-button", 292 "subviewbutton", 293 "subviewbutton-iconic" 294 ); 295 item.setAttribute("targetURI", tabInfo.url); 296 item.setAttribute( 297 "label", 298 tabInfo.title != "" ? tabInfo.title : tabInfo.url 299 ); 300 if (tabInfo.icon) { 301 let icon = tabInfo.icon; 302 if (gSync.REMOTE_SVG_ICON_DECODING) { 303 try { 304 const uri = NetUtil.newURI(icon); 305 if (!TRUSTED_FAVICON_SCHEMES.includes(uri.scheme)) { 306 const size = Math.floor(16 * window.devicePixelRatio); 307 icon = getMozRemoteImageURL(uri.spec, size); 308 } 309 } catch (e) { 310 console.error(e); 311 icon = ""; 312 } 313 } 314 item.setAttribute("image", icon); 315 } 316 item.setAttribute("tooltiptext", tooltipText); 317 // We need to use "click" instead of "command" here so openUILink 318 // respects different buttons (eg, to open in a new tab). 319 item.addEventListener("click", e => { 320 // We want to differentiate between when the fxa panel is within the app menu/hamburger bar 321 let object = window.gSync._getEntryPointForElement(e.currentTarget); 322 SyncedTabs.recordSyncedTabsTelemetry(object, "click", { 323 tab_pos: index.toString(), 324 }); 325 document.defaultView.openUILink(tabInfo.url, e, { 326 triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( 327 {} 328 ), 329 }); 330 if (BrowserUtils.whereToOpenLink(e) != "current") { 331 e.preventDefault(); 332 e.stopPropagation(); 333 } else { 334 CustomizableUI.hidePanelForNode(item); 335 } 336 }); 337 tabContainer.appendChild(item); 338 // We should only add an X button next to tabs if the device 339 // is broadcasting that it can remotely close tabs 340 if (canCloseTabs) { 341 let closeBtn = this._createCloseTabElement(tabInfo.url, device); 342 closeBtn.tab = item; 343 tabContainer.appendChild(closeBtn); 344 let undoBtn = this._createUndoCloseTabElement(tabInfo.url, device); 345 undoBtn.tab = item; 346 tabContainer.appendChild(undoBtn); 347 } 348 return tabContainer; 349 } 350 351 _createShowMoreSyncedTabsElement(paginationInfo) { 352 let showMoreItem = document.createXULElement("toolbarbutton"); 353 showMoreItem.setAttribute("itemtype", "showmorebutton"); 354 showMoreItem.setAttribute("closemenu", "none"); 355 showMoreItem.classList.add("subviewbutton", "subviewbutton-nav-down"); 356 document.l10n.setAttributes(showMoreItem, "appmenu-remote-tabs-showmore"); 357 358 paginationInfo.maxTabs = Infinity; 359 showMoreItem.addEventListener("click", e => { 360 e.preventDefault(); 361 e.stopPropagation(); 362 this._showSyncedTabs(paginationInfo); 363 }); 364 return showMoreItem; 365 } 366 367 _createShowInactiveTabsElement(client, device) { 368 let showItem = document.createXULElement("toolbarbutton"); 369 showItem.setAttribute("itemtype", "showinactivebutton"); 370 showItem.setAttribute("closemenu", "none"); 371 showItem.classList.add("subviewbutton", "subviewbutton-nav"); 372 document.l10n.setAttributes( 373 showItem, 374 "appmenu-remote-tabs-show-inactive-tabs" 375 ); 376 377 let canClose = 378 device && fxAccounts.commands.closeTab.isDeviceCompatible(device); 379 380 showItem.addEventListener("click", e => { 381 let node = PanelMultiView.getViewNode( 382 document, 383 "PanelUI-fxa-menu-inactive-tabs" 384 ); 385 386 // device name. 387 let label = node.querySelector("label[itemtype='client']"); 388 label.textContent = client.name; 389 390 // Update the tab list. 391 let container = node.querySelector(".panel-subview-body"); 392 container.replaceChildren( 393 ...client.tabs 394 .filter(t => t.inactive) 395 .map((tab, index) => 396 this._createSyncedTabElement(tab, index, device, canClose) 397 ) 398 ); 399 PanelUI.showSubView("PanelUI-fxa-menu-inactive-tabs", showItem, e); 400 }); 401 return showItem; 402 } 403 404 _createCloseTabElement(url, device) { 405 let closeBtn = document.createXULElement("toolbarbutton"); 406 closeBtn.classList.add( 407 "remote-tabs-close-button", 408 "all-tabs-close-button", 409 "subviewbutton" 410 ); 411 closeBtn.setAttribute("closemenu", "none"); 412 closeBtn.setAttribute( 413 "tooltiptext", 414 gSync.fluentStrings.formatValueSync("synced-tabs-context-close-tab", { 415 deviceName: device.name, 416 }) 417 ); 418 closeBtn.addEventListener("click", e => { 419 e.stopPropagation(); 420 421 let tabContainer = closeBtn.parentNode; 422 let tabList = tabContainer.parentNode; 423 424 let undoBtn = tabContainer.querySelector(".remote-tabs-undo-button"); 425 426 let prevClose = tabList.querySelector( 427 ".remote-tabs-undo-button:not([hidden])" 428 ); 429 if (prevClose) { 430 let prevCloseContainer = prevClose.parentNode; 431 prevCloseContainer.classList.add("tabitem-removed"); 432 prevCloseContainer.addEventListener("transitionend", () => { 433 prevCloseContainer.remove(); 434 }); 435 } 436 closeBtn.hidden = true; 437 undoBtn.hidden = false; 438 // This tab has been closed so we prevent the user from 439 // interacting with it 440 if (closeBtn.tab) { 441 closeBtn.tab.disabled = true; 442 } 443 // The user could be hitting multiple tabs across multiple devices, with a few 444 // seconds in-between -- we should not immediately fire off pushes, so we 445 // add it to a queue and send in bulk at a later time 446 SyncedTabsManagement.enqueueTabToClose(device.id, url); 447 }); 448 return closeBtn; 449 } 450 451 _createUndoCloseTabElement(url, device) { 452 let undoBtn = document.createXULElement("toolbarbutton"); 453 undoBtn.classList.add("remote-tabs-undo-button", "subviewbutton"); 454 undoBtn.setAttribute("closemenu", "none"); 455 undoBtn.setAttribute("data-l10n-id", "text-action-undo"); 456 undoBtn.hidden = true; 457 458 undoBtn.addEventListener("click", function (e) { 459 e.stopPropagation(); 460 461 undoBtn.hidden = true; 462 let closeBtn = undoBtn.parentNode.querySelector(".all-tabs-close-button"); 463 closeBtn.hidden = false; 464 if (undoBtn.tab) { 465 undoBtn.tab.disabled = false; 466 } 467 468 // remove this tab from being remotely closed 469 SyncedTabsManagement.removePendingTabToClose(device.id, url); 470 }); 471 return undoBtn; 472 } 473 474 destroy() { 475 Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED); 476 this.tabsList = null; 477 this.deck = null; 478 this.separator = null; 479 } 480 }; 481 482 var gSync = { 483 _initialized: false, 484 _isCurrentlySyncing: false, 485 // The last sync start time. Used to calculate the leftover animation time 486 // once syncing completes (bug 1239042). 487 _syncStartTime: 0, 488 _syncAnimationTimer: 0, 489 _obs: ["weave:engine:sync:finish", "quit-application", UIState.ON_UPDATE], 490 491 get log() { 492 if (!this._log) { 493 const { Log } = ChromeUtils.importESModule( 494 "resource://gre/modules/Log.sys.mjs" 495 ); 496 let syncLog = Log.repository.getLogger("Sync.Browser"); 497 syncLog.manageLevelFromPref("services.sync.log.logger.browser"); 498 this._log = syncLog; 499 } 500 return this._log; 501 }, 502 503 get fluentStrings() { 504 delete this.fluentStrings; 505 return (this.fluentStrings = new Localization( 506 [ 507 "branding/brand.ftl", 508 "browser/accounts.ftl", 509 "browser/appmenu.ftl", 510 "browser/sync.ftl", 511 "browser/syncedTabs.ftl", 512 "browser/newtab/asrouter.ftl", 513 ], 514 true 515 )); 516 }, 517 518 // Returns true if FxA is configured, but the send tab targets list isn't 519 // ready yet. 520 get sendTabConfiguredAndLoading() { 521 const state = UIState.get(); 522 return ( 523 state.status == UIState.STATUS_SIGNED_IN && 524 state.syncEnabled && 525 !fxAccounts.device.recentDeviceList 526 ); 527 }, 528 529 get isSignedIn() { 530 return UIState.get().status == UIState.STATUS_SIGNED_IN; 531 }, 532 533 shouldHideSendContextMenuItems(enabled) { 534 const state = UIState.get(); 535 // Only show the "Send..." context menu items when sending would be possible 536 if ( 537 enabled && 538 state.status == UIState.STATUS_SIGNED_IN && 539 state.syncEnabled && 540 this.getSendTabTargets().length 541 ) { 542 return false; 543 } 544 return true; 545 }, 546 547 getSendTabTargets() { 548 const targets = []; 549 const state = UIState.get(); 550 if ( 551 state.status != UIState.STATUS_SIGNED_IN || 552 !state.syncEnabled || 553 !fxAccounts.device.recentDeviceList 554 ) { 555 return targets; 556 } 557 for (let d of fxAccounts.device.recentDeviceList) { 558 if (d.isCurrentDevice) { 559 continue; 560 } 561 562 if (fxAccounts.commands.sendTab.isDeviceCompatible(d)) { 563 targets.push(d); 564 } 565 } 566 return targets.sort((a, b) => b.lastAccessTime - a.lastAccessTime); 567 }, 568 569 _definePrefGetters() { 570 XPCOMUtils.defineLazyPreferenceGetter( 571 this, 572 "FXA_ENABLED", 573 "identity.fxaccounts.enabled" 574 ); 575 XPCOMUtils.defineLazyPreferenceGetter( 576 this, 577 "FXA_CTA_MENU_ENABLED", 578 "identity.fxaccounts.toolbar.pxiToolbarEnabled" 579 ); 580 XPCOMUtils.defineLazyPreferenceGetter( 581 this, 582 "REMOTE_SVG_ICON_DECODING", 583 "browser.tabs.remoteSVGIconDecoding" 584 ); 585 }, 586 587 maybeUpdateUIState() { 588 // Update the UI. 589 if (UIState.isReady()) { 590 const state = UIState.get(); 591 // If we are not configured, the UI is already in the right state when 592 // we open the window. We can avoid a repaint. 593 if (state.status != UIState.STATUS_NOT_CONFIGURED) { 594 this.updateAllUI(state); 595 } 596 } 597 }, 598 599 init() { 600 if (this._initialized) { 601 return; 602 } 603 604 this._definePrefGetters(); 605 606 if (!this.FXA_ENABLED) { 607 this.onFxaDisabled(); 608 return; 609 } 610 611 MozXULElement.insertFTLIfNeeded("browser/sync.ftl"); 612 MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl"); 613 614 // Label for the sync buttons. 615 const appMenuLabel = PanelMultiView.getViewNode( 616 document, 617 "appMenu-fxa-label2" 618 ); 619 if (!appMenuLabel) { 620 // We are in a window without our elements - just abort now, without 621 // setting this._initialized, so we don't attempt to remove observers. 622 return; 623 } 624 // We start with every menuitem hidden (except for the "setup sync" state), 625 // so that we don't need to init the sync UI on windows like pageInfo.xhtml 626 // (see bug 1384856). 627 // maybeUpdateUIState() also optimizes for this - if we should be in the 628 // "setup sync" state, that function assumes we are already in it and 629 // doesn't re-initialize the UI elements. 630 document.getElementById("sync-setup").hidden = false; 631 PanelMultiView.getViewNode( 632 document, 633 "PanelUI-remotetabs-setupsync" 634 ).hidden = false; 635 636 const appMenuHeaderTitle = PanelMultiView.getViewNode( 637 document, 638 "appMenu-header-title" 639 ); 640 const appMenuHeaderDescription = PanelMultiView.getViewNode( 641 document, 642 "appMenu-header-description" 643 ); 644 const appMenuHeaderText = PanelMultiView.getViewNode( 645 document, 646 "appMenu-fxa-text" 647 ); 648 appMenuHeaderTitle.hidden = true; 649 // We must initialize the label attribute here instead of the markup 650 // due to a timing error. The fluent label attribute was being applied 651 // after we had updated appMenuLabel and thus displayed an incorrect 652 // label for signed in users. 653 const [headerDesc, headerText] = this.fluentStrings.formatValuesSync([ 654 "appmenu-fxa-signed-in-label", 655 "appmenu-fxa-sync-and-save-data2", 656 ]); 657 appMenuHeaderDescription.value = headerDesc; 658 appMenuHeaderText.textContent = headerText; 659 660 for (let topic of this._obs) { 661 Services.obs.addObserver(this, topic, true); 662 } 663 664 this.maybeUpdateUIState(); 665 666 EnsureFxAccountsWebChannel(); 667 668 let fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa"); 669 fxaPanelView.addEventListener("ViewShowing", this); 670 fxaPanelView.addEventListener("ViewHiding", this); 671 fxaPanelView.addEventListener("command", this); 672 PanelMultiView.getViewNode( 673 document, 674 "PanelUI-fxa-menu-syncnow-button" 675 ).addEventListener("mouseover", this); 676 PanelMultiView.getViewNode( 677 document, 678 "PanelUI-fxa-menu-sendtab-not-configured-button" 679 ).addEventListener("command", this); 680 PanelMultiView.getViewNode( 681 document, 682 "PanelUI-fxa-menu-sendtab-connect-device-button" 683 ).addEventListener("command", this); 684 685 PanelUI.mainView.addEventListener("ViewShowing", this); 686 687 // If the experiment is enabled, we'll need to update the panels 688 // to show some different text to the user 689 if (this.FXA_CTA_MENU_ENABLED) { 690 this.updateFxAPanel(UIState.get()); 691 this.updateCTAPanel(); 692 } 693 694 const avatarIconVariant = 695 NimbusFeatures.fxaButtonVisibility.getVariable("avatarIconVariant"); 696 if (avatarIconVariant) { 697 this.applyAvatarIconVariant(avatarIconVariant); 698 } 699 700 this._initialized = true; 701 }, 702 703 uninit() { 704 if (!this._initialized) { 705 return; 706 } 707 708 for (let topic of this._obs) { 709 Services.obs.removeObserver(this, topic); 710 } 711 712 this._initialized = false; 713 }, 714 715 handleEvent(event) { 716 switch (event.type) { 717 case "mouseover": 718 this.refreshSyncButtonsTooltip(); 719 break; 720 case "command": { 721 this.onCommand(event.target); 722 break; 723 } 724 case "ViewShowing": { 725 if (event.target == PanelUI.mainView) { 726 this.onAppMenuShowing(); 727 } else { 728 this.onFxAPanelViewShowing(event.target); 729 } 730 break; 731 } 732 case "ViewHiding": { 733 this.onFxAPanelViewHiding(event.target); 734 } 735 } 736 }, 737 738 onAppMenuShowing() { 739 const appMenuHeaderText = PanelMultiView.getViewNode( 740 document, 741 "appMenu-fxa-text" 742 ); 743 744 const ctaDefaultStringID = "appmenu-fxa-sync-and-save-data2"; 745 const ctaStringID = this.getMenuCtaCopy(NimbusFeatures.fxaAppMenuItem); 746 747 document.l10n.setAttributes( 748 appMenuHeaderText, 749 ctaStringID || ctaDefaultStringID 750 ); 751 752 if (NimbusFeatures.fxaAppMenuItem.getVariable("ctaCopyVariant")) { 753 NimbusFeatures.fxaAppMenuItem.recordExposureEvent(); 754 } 755 }, 756 757 onFxAPanelViewShowing(panelview) { 758 let messageId = panelview.getAttribute( 759 MenuMessage.SHOWING_FXA_MENU_MESSAGE_ATTR 760 ); 761 if (messageId) { 762 MenuMessage.recordMenuMessageTelemetry( 763 "IMPRESSION", 764 MenuMessage.SOURCES.PXI_MENU, 765 messageId 766 ); 767 let message = ASRouter.getMessageById(messageId); 768 ASRouter.addImpression(message); 769 } 770 771 let syncNowBtn = panelview.querySelector(".syncnow-label"); 772 let l10nId = syncNowBtn.getAttribute( 773 this._isCurrentlySyncing 774 ? "syncing-data-l10n-id" 775 : "sync-now-data-l10n-id" 776 ); 777 document.l10n.setAttributes(syncNowBtn, l10nId); 778 779 // This needs to exist because if the user is signed in 780 // but the user disabled or disconnected sync we should not show the button 781 const syncPrefsButtonEl = PanelMultiView.getViewNode( 782 document, 783 "PanelUI-fxa-menu-sync-prefs-button" 784 ); 785 const syncEnabled = UIState.get().syncEnabled; 786 syncPrefsButtonEl.hidden = !syncEnabled; 787 if (!syncEnabled) { 788 this._disableSyncOffIndicator(); 789 } 790 791 // We should ensure that we do not show the sign out button 792 // if the user is not signed in 793 const signOutButtonEl = PanelMultiView.getViewNode( 794 document, 795 "PanelUI-fxa-menu-account-signout-button" 796 ); 797 signOutButtonEl.hidden = !this.isSignedIn; 798 799 panelview.syncedTabsPanelList = new SyncedTabsPanelList( 800 panelview, 801 PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-deck"), 802 PanelMultiView.getViewNode(document, "PanelUI-fxa-remotetabs-tabslist"), 803 PanelMultiView.getViewNode(document, "PanelUI-remote-tabs-separator") 804 ); 805 806 // Any variant on the CTA will have been applied inside of updateFxAPanel, 807 // but now that the panel is showing, we record exposure. 808 const ctaCopyVariant = 809 NimbusFeatures.fxaAvatarMenuItem.getVariable("ctaCopyVariant"); 810 if (ctaCopyVariant) { 811 NimbusFeatures.fxaAvatarMenuItem.recordExposureEvent(); 812 } 813 }, 814 815 onFxAPanelViewHiding(panelview) { 816 MenuMessage.hidePxiMenuMessage(gBrowser.selectedBrowser); 817 panelview.syncedTabsPanelList.destroy(); 818 panelview.syncedTabsPanelList = null; 819 }, 820 821 onCommand(button) { 822 switch (button.id) { 823 case "PanelUI-fxa-menu-sync-prefs-button": 824 this.openPrefsFromFxaMenu("sync_settings", button); 825 break; 826 case "PanelUI-fxa-menu-setup-sync-button": 827 this.openSyncSetup("sync_settings", button); 828 break; 829 830 case "PanelUI-fxa-menu-sendtab-connect-device-button": 831 // fall through 832 case "PanelUI-fxa-menu-connect-device-button": 833 this.clickOpenConnectAnotherDevice(button); 834 break; 835 836 case "fxa-manage-account-button": 837 this.clickFxAMenuHeaderButton(button); 838 break; 839 case "PanelUI-fxa-menu-syncnow-button": 840 this.doSyncFromFxaMenu(button); 841 break; 842 case "PanelUI-fxa-menu-sendtab-button": 843 this.showSendToDeviceViewFromFxaMenu(button); 844 break; 845 case "PanelUI-fxa-menu-account-signout-button": 846 this.disconnect(); 847 break; 848 case "PanelUI-fxa-menu-monitor-button": 849 this.openMonitorLink(button); 850 break; 851 case "PanelUI-services-menu-relay-button": 852 case "PanelUI-fxa-menu-relay-button": 853 this.openRelayLink(button); 854 break; 855 case "PanelUI-fxa-menu-vpn-button": 856 this.openVPNLink(button); 857 break; 858 case "PanelUI-fxa-menu-sendtab-not-configured-button": 859 this.openSyncSetup("send_tab", button); 860 break; 861 } 862 }, 863 864 observe(subject, topic, data) { 865 if (!this._initialized) { 866 console.error("browser-sync observer called after unload: ", topic); 867 return; 868 } 869 switch (topic) { 870 case UIState.ON_UPDATE: { 871 const state = UIState.get(); 872 this.updateAllUI(state); 873 break; 874 } 875 case "quit-application": 876 // Stop the animation timer on shutdown, since we can't update the UI 877 // after this. 878 clearTimeout(this._syncAnimationTimer); 879 break; 880 case "weave:engine:sync:finish": 881 if (data != "clients") { 882 return; 883 } 884 this.onClientsSynced(); 885 this.updateFxAPanel(UIState.get()); 886 break; 887 } 888 }, 889 890 updateAllUI(state) { 891 this.updatePanelPopup(state); 892 this.updateState(state); 893 this.updateSyncButtonsTooltip(state); 894 this.updateSyncStatus(state); 895 this.updateFxAPanel(state); 896 this.ensureFxaDevices(); 897 this.fetchListOfOAuthClients(); 898 }, 899 900 // Ensure we have *something* in `fxAccounts.device.recentDeviceList` as some 901 // of our UI logic depends on it not being null. When FxA is notified of a 902 // device change it will auto refresh `recentDeviceList`, and all UI which 903 // shows the device list will start with `recentDeviceList`, but should also 904 // force a refresh, both of which should mean in the worst-case, the UI is up 905 // to date after a very short delay. 906 async ensureFxaDevices() { 907 if (UIState.get().status != UIState.STATUS_SIGNED_IN) { 908 console.info("Skipping device list refresh; not signed in"); 909 return; 910 } 911 if (!fxAccounts.device.recentDeviceList) { 912 if (await this.refreshFxaDevices()) { 913 // Assuming we made the call successfully it should be impossible to end 914 // up with a falsey recentDeviceList, so make noise if that's false. 915 if (!fxAccounts.device.recentDeviceList) { 916 console.warn("Refreshing device list didn't find any devices."); 917 } 918 } 919 } 920 }, 921 922 // Force a refresh of the fxa device list. Note that while it's theoretically 923 // OK to call `fxAccounts.device.refreshDeviceList` multiple times concurrently 924 // and regularly, this call tells it to avoid those protections, so will always 925 // hit the FxA servers - therefore, you should be very careful how often you 926 // call this. 927 // Returns Promise<bool> to indicate whether a refresh was actually done. 928 async refreshFxaDevices() { 929 if (UIState.get().status != UIState.STATUS_SIGNED_IN) { 930 console.info("Skipping device list refresh; not signed in"); 931 return false; 932 } 933 try { 934 // Do the actual refresh telling it to avoid the "flooding" protections. 935 await fxAccounts.device.refreshDeviceList({ ignoreCached: true }); 936 return true; 937 } catch (e) { 938 this.log.error("Refreshing device list failed.", e); 939 return false; 940 } 941 }, 942 943 /** 944 * Potential network call. Fetch the list of OAuth clients attached to the current Mozilla account. 945 * 946 * @returns {Promise<boolean>} - Resolves to true if successful, false otherwise. 947 */ 948 async fetchListOfOAuthClients() { 949 if (!this.isSignedIn) { 950 console.info("Skipping fetching other attached clients"); 951 return false; 952 } 953 try { 954 this._attachedClients = await fxAccounts.listAttachedOAuthClients(); 955 return true; 956 } catch (e) { 957 this.log.error("Could not fetch attached OAuth clients", e); 958 return false; 959 } 960 }, 961 962 updateSendToDeviceTitle() { 963 const tabCount = gBrowser.selectedTab.multiselected 964 ? gBrowser.selectedTabs.length 965 : 1; 966 document.l10n.setArgs( 967 PanelMultiView.getViewNode(document, "PanelUI-fxa-menu-sendtab-button"), 968 { tabCount } 969 ); 970 }, 971 972 showSendToDeviceView(anchor) { 973 PanelUI.showSubView("PanelUI-sendTabToDevice", anchor); 974 let panelViewNode = document.getElementById("PanelUI-sendTabToDevice"); 975 this._populateSendTabToDevicesView(panelViewNode); 976 }, 977 978 showSendToDeviceViewFromFxaMenu(anchor) { 979 const state = UIState.get(); 980 if (state.status !== UIState.STATUS_SIGNED_IN || !state.syncEnabled) { 981 PanelUI.showSubView("PanelUI-fxa-menu-sendtab-not-configured", anchor); 982 return; 983 } 984 985 const targets = this.sendTabConfiguredAndLoading 986 ? [] 987 : this.getSendTabTargets(); 988 if (!targets.length) { 989 PanelUI.showSubView("PanelUI-fxa-menu-sendtab-no-devices", anchor); 990 return; 991 } 992 993 this.showSendToDeviceView(anchor); 994 this.emitFxaToolbarTelemetry("send_tab", anchor); 995 }, 996 997 _populateSendTabToDevicesView(panelViewNode, reloadDevices = true) { 998 let bodyNode = panelViewNode.querySelector(".panel-subview-body"); 999 let panelNode = panelViewNode.closest("panel"); 1000 let browser = gBrowser.selectedBrowser; 1001 let uri = browser.currentURI; 1002 let title = browser.contentTitle; 1003 let multiselected = gBrowser.selectedTab.multiselected; 1004 1005 // This is on top because it also clears the device list between state 1006 // changes. 1007 this.populateSendTabToDevicesMenu( 1008 bodyNode, 1009 uri, 1010 title, 1011 multiselected, 1012 (clientId, name, clientType, lastModified) => { 1013 if (!name) { 1014 return document.createXULElement("toolbarseparator"); 1015 } 1016 let item = document.createXULElement("toolbarbutton"); 1017 item.setAttribute("wrap", true); 1018 item.setAttribute("align", "start"); 1019 item.classList.add("sendToDevice-device", "subviewbutton"); 1020 if (clientId) { 1021 item.classList.add("subviewbutton-iconic"); 1022 if (lastModified) { 1023 let lastSyncDate = gSync.formatLastSyncDate(lastModified); 1024 if (lastSyncDate) { 1025 item.setAttribute( 1026 "tooltiptext", 1027 this.fluentStrings.formatValueSync("appmenu-fxa-last-sync", { 1028 time: lastSyncDate, 1029 }) 1030 ); 1031 } 1032 } 1033 } 1034 1035 item.addEventListener("command", () => { 1036 if (panelNode) { 1037 PanelMultiView.hidePopup(panelNode); 1038 } 1039 }); 1040 return item; 1041 }, 1042 true 1043 ); 1044 1045 bodyNode.removeAttribute("state"); 1046 // If the app just started, we won't have fetched the device list yet. Sync 1047 // does this automatically ~10 sec after startup, but there's no trigger for 1048 // this if we're signed in to FxA, but not Sync. 1049 if (gSync.sendTabConfiguredAndLoading) { 1050 bodyNode.setAttribute("state", "notready"); 1051 } 1052 if (reloadDevices) { 1053 // Force a refresh of the fxa device list in case the user connected a new 1054 // device, and is waiting for it to show up. 1055 this.refreshFxaDevices().then(_ => { 1056 if (!window.closed) { 1057 this._populateSendTabToDevicesView(panelViewNode, false); 1058 } 1059 }); 1060 } 1061 }, 1062 1063 async toggleAccountPanel(anchor = null, aEvent) { 1064 // Don't show the panel if the window is in customization mode. 1065 if (document.documentElement.hasAttribute("customizing")) { 1066 return; 1067 } 1068 1069 if ( 1070 (aEvent.type == "mousedown" && aEvent.button != 0) || 1071 (aEvent.type == "keypress" && 1072 aEvent.charCode != KeyEvent.DOM_VK_SPACE && 1073 aEvent.keyCode != KeyEvent.DOM_VK_RETURN) 1074 ) { 1075 return; 1076 } 1077 1078 const fxaToolbarMenuBtn = document.getElementById( 1079 "fxa-toolbar-menu-button" 1080 ); 1081 1082 if (anchor === null) { 1083 anchor = fxaToolbarMenuBtn; 1084 } 1085 1086 if (anchor == fxaToolbarMenuBtn && anchor.getAttribute("open") != "true") { 1087 if (ASRouter.initialized) { 1088 await ASRouter.sendTriggerMessage({ 1089 browser: gBrowser.selectedBrowser, 1090 id: "menuOpened", 1091 context: { source: MenuMessage.SOURCES.PXI_MENU }, 1092 }); 1093 } 1094 } 1095 1096 // We read the state that's been set on the root node, since that makes 1097 // it easier to test the various front-end states without having to actually 1098 // have UIState know about it. 1099 let fxaStatus = document.documentElement.getAttribute("fxastatus"); 1100 1101 if (fxaStatus == "not_configured") { 1102 // sign in button in app (hamburger) menu 1103 // should take you straight to fxa sign in page 1104 if (anchor.id == "appMenu-fxa-label2") { 1105 this.openFxAEmailFirstPageFromFxaMenu(anchor); 1106 PanelUI.hide(); 1107 return; 1108 } 1109 1110 // If we're signed out but have the PXI pref enabled 1111 // we should show the PXI panel instead of taking the user 1112 // straight to FxA sign-in 1113 if (this.FXA_CTA_MENU_ENABLED) { 1114 this.updateFxAPanel(UIState.get()); 1115 this.updateCTAPanel(anchor); 1116 PanelUI.showSubView("PanelUI-fxa", anchor, aEvent); 1117 } else if (anchor == fxaToolbarMenuBtn) { 1118 // The fxa toolbar button doesn't have much context before the user 1119 // clicks it so instead of going straight to the login page, 1120 // we take them to a page that has more information 1121 this.emitFxaToolbarTelemetry("toolbar_icon", anchor); 1122 openTrustedLinkIn("about:preferences#sync", "tab"); 1123 PanelUI.hide(); 1124 } 1125 return; 1126 } 1127 // If the user is signed in and we have the PXI pref enabled then add 1128 // the pxi panel to the existing toolbar 1129 if (this.FXA_CTA_MENU_ENABLED) { 1130 this.updateCTAPanel(anchor); 1131 } 1132 1133 if (!gFxaToolbarAccessed) { 1134 Services.prefs.setBoolPref("identity.fxaccounts.toolbar.accessed", true); 1135 } 1136 1137 this.enableSendTabIfValidTab(); 1138 1139 if (!this.getSendTabTargets().length) { 1140 for (const id of [ 1141 "PanelUI-fxa-menu-sendtab-button", 1142 "PanelUI-fxa-menu-sendtab-separator", 1143 ]) { 1144 PanelMultiView.getViewNode(document, id).hidden = true; 1145 } 1146 } 1147 1148 if (anchor.getAttribute("open") == "true") { 1149 PanelUI.hide(); 1150 } else { 1151 this.emitFxaToolbarTelemetry("toolbar_icon", anchor); 1152 PanelUI.showSubView("PanelUI-fxa", anchor, aEvent); 1153 } 1154 }, 1155 1156 _disableSyncOffIndicator() { 1157 const SYNC_PANEL_ACCESSED_PREF = 1158 "identity.fxaccounts.toolbar.syncSetup.panelAccessed"; 1159 if (!Services.prefs.getBoolPref(SYNC_PANEL_ACCESSED_PREF, false)) { 1160 // Turn off the indicator so the user doesn't see it in subsequent openings 1161 Services.prefs.setBoolPref(SYNC_PANEL_ACCESSED_PREF, true); 1162 } 1163 }, 1164 1165 _shouldShowSyncOffIndicator() { 1166 // We only ever want to show the user the dot once, once they've clicked into the panel 1167 // we do not show them the dot anymore 1168 return !Services.prefs.getBoolPref( 1169 "identity.fxaccounts.toolbar.syncSetup.panelAccessed", 1170 false 1171 ); 1172 }, 1173 1174 updateFxAPanel(state = {}) { 1175 const expandedSignInCopy = 1176 NimbusFeatures.expandSignInButton.getVariable("ctaCopyVariant"); 1177 const mainWindowEl = document.documentElement; 1178 1179 const menuHeaderTitleEl = PanelMultiView.getViewNode( 1180 document, 1181 "fxa-menu-header-title" 1182 ); 1183 const menuHeaderDescriptionEl = PanelMultiView.getViewNode( 1184 document, 1185 "fxa-menu-header-description" 1186 ); 1187 const cadButtonEl = PanelMultiView.getViewNode( 1188 document, 1189 "PanelUI-fxa-menu-connect-device-button" 1190 ); 1191 const syncNowButtonEl = PanelMultiView.getViewNode( 1192 document, 1193 "PanelUI-fxa-menu-syncnow-button" 1194 ); 1195 const fxaMenuAccountButtonEl = PanelMultiView.getViewNode( 1196 document, 1197 "fxa-manage-account-button" 1198 ); 1199 const signedInContainer = PanelMultiView.getViewNode( 1200 document, 1201 "PanelUI-signedin-panel" 1202 ); 1203 const emptyProfilesButton = PanelMultiView.getViewNode( 1204 document, 1205 "PanelUI-fxa-menu-empty-profiles-button" 1206 ); 1207 const profilesButton = PanelMultiView.getViewNode( 1208 document, 1209 "PanelUI-fxa-menu-profiles-button" 1210 ); 1211 const profilesSeparator = PanelMultiView.getViewNode( 1212 document, 1213 "PanelUI-fxa-menu-profiles-separator" 1214 ); 1215 const syncSetupEl = PanelMultiView.getViewNode( 1216 document, 1217 "PanelUI-fxa-menu-setup-sync-container" 1218 ); 1219 const fxaToolbarMenuButton = document.getElementById( 1220 "fxa-toolbar-menu-button" 1221 ); 1222 const syncSetupSeparator = PanelMultiView.getViewNode( 1223 document, 1224 "PanelUI-set-up-sync-separator" 1225 ); 1226 1227 let fxaAvatarLabelEl = document.getElementById("fxa-avatar-label"); 1228 1229 // Reset FxA/Sync UI elements to default, which is signed out 1230 cadButtonEl.setAttribute("disabled", true); 1231 syncNowButtonEl.hidden = true; 1232 signedInContainer.hidden = true; 1233 fxaMenuAccountButtonEl.classList.remove("subviewbutton-nav"); 1234 fxaMenuAccountButtonEl.removeAttribute("closemenu"); 1235 menuHeaderDescriptionEl.hidden = false; 1236 1237 // Expanded sign in copy experiment is only for signed out users 1238 // so if a text variant has been provided then we show the expanded label 1239 // otherwise it'll be the default avatar icon 1240 // fxaToolbarMenuButton can be null in certain testing scenarios 1241 if (fxaToolbarMenuButton) { 1242 if ( 1243 state.status === UIState.STATUS_NOT_CONFIGURED && 1244 expandedSignInCopy 1245 ) { 1246 fxaAvatarLabelEl.setAttribute( 1247 "value", 1248 this.fluentStrings.formatValueSync(expandedSignInCopy) 1249 ); 1250 fxaAvatarLabelEl.removeAttribute("hidden"); 1251 fxaToolbarMenuButton.setAttribute("data-l10n-id", "fxa-avatar-tooltip"); 1252 fxaToolbarMenuButton.classList.add("avatar-button-background"); 1253 } else { 1254 // Either signed in, or experiment not enabled 1255 fxaToolbarMenuButton.setAttribute( 1256 "data-l10n-id", 1257 "toolbar-button-account" 1258 ); 1259 fxaToolbarMenuButton.classList.remove("avatar-button-background"); 1260 fxaAvatarLabelEl.hidden = true; 1261 } 1262 } 1263 1264 // The Firefox Account toolbar currently handles 3 different states for 1265 // users. The default `not_configured` state shows an empty avatar, `unverified` 1266 // state shows an avatar with an email icon, `login-failed` state shows an avatar 1267 // with a danger icon and the `verified` state will show the users 1268 // custom profile image or a filled avatar. 1269 let stateValue = "not_configured"; 1270 let headerTitleL10nId; 1271 let headerDescription; 1272 1273 switch (state.status) { 1274 case UIState.STATUS_NOT_CONFIGURED: 1275 mainWindowEl.style.removeProperty("--avatar-image-url"); 1276 headerTitleL10nId = this.FXA_CTA_MENU_ENABLED 1277 ? "synced-tabs-fxa-sign-in" 1278 : "appmenuitem-sign-in-account"; 1279 headerDescription = this.fluentStrings.formatValueSync( 1280 this.FXA_CTA_MENU_ENABLED 1281 ? "fxa-menu-sync-description" 1282 : "appmenu-fxa-signed-in-label" 1283 ); 1284 if (this.FXA_CTA_MENU_ENABLED) { 1285 const ctaCopy = this.getMenuCtaCopy(NimbusFeatures.fxaAvatarMenuItem); 1286 if (ctaCopy) { 1287 headerTitleL10nId = ctaCopy.headerTitleL10nId; 1288 headerDescription = ctaCopy.headerDescription; 1289 } 1290 } 1291 1292 // Reposition profiles elements 1293 emptyProfilesButton.remove(); 1294 profilesButton.remove(); 1295 profilesSeparator.remove(); 1296 1297 profilesSeparator.hidden = true; 1298 1299 signedInContainer.after(profilesSeparator); 1300 signedInContainer.after(profilesButton); 1301 signedInContainer.after(emptyProfilesButton); 1302 1303 break; 1304 1305 case UIState.STATUS_LOGIN_FAILED: 1306 stateValue = "login-failed"; 1307 headerTitleL10nId = "account-disconnected2"; 1308 headerDescription = state.displayName || state.email; 1309 mainWindowEl.style.removeProperty("--avatar-image-url"); 1310 break; 1311 1312 case UIState.STATUS_NOT_VERIFIED: 1313 stateValue = "unverified"; 1314 headerTitleL10nId = "account-finish-account-setup"; 1315 headerDescription = state.displayName || state.email; 1316 break; 1317 1318 case UIState.STATUS_SIGNED_IN: 1319 stateValue = "signedin"; 1320 headerTitleL10nId = "appmenuitem-fxa-manage-account"; 1321 headerDescription = state.displayName || state.email; 1322 this.updateAvatarURL( 1323 mainWindowEl, 1324 state.avatarURL, 1325 state.avatarIsDefault 1326 ); 1327 signedInContainer.hidden = false; 1328 cadButtonEl.removeAttribute("disabled"); 1329 1330 if (state.syncEnabled) { 1331 // Always show sync now and connect another device button when sync is enabled 1332 syncNowButtonEl.removeAttribute("hidden"); 1333 cadButtonEl.removeAttribute("hidden"); 1334 syncSetupEl.setAttribute("hidden", "true"); 1335 } else { 1336 if (this._shouldShowSyncOffIndicator()) { 1337 fxaToolbarMenuButton?.setAttribute("badge-status", "sync-disabled"); 1338 } 1339 syncSetupEl.removeAttribute("hidden"); 1340 } 1341 1342 if (state.hasSyncKeys) { 1343 cadButtonEl.removeAttribute("hidden"); 1344 syncSetupSeparator.removeAttribute("hidden"); 1345 } else { 1346 cadButtonEl.setAttribute("hidden", "true"); 1347 syncSetupSeparator.setAttribute("hidden", "true"); 1348 } 1349 1350 // Reposition profiles elements 1351 emptyProfilesButton.remove(); 1352 profilesButton.remove(); 1353 profilesSeparator.remove(); 1354 1355 profilesSeparator.hidden = false; 1356 1357 fxaMenuAccountButtonEl.after(profilesSeparator); 1358 fxaMenuAccountButtonEl.after(profilesButton); 1359 fxaMenuAccountButtonEl.after(emptyProfilesButton); 1360 1361 break; 1362 1363 default: 1364 headerTitleL10nId = this.FXA_CTA_MENU_ENABLED 1365 ? "synced-tabs-fxa-sign-in" 1366 : "appmenuitem-sign-in-account"; 1367 headerDescription = this.fluentStrings.formatValueSync( 1368 "fxa-menu-turn-on-sync-default" 1369 ); 1370 break; 1371 } 1372 1373 // Update UI elements with determined values 1374 mainWindowEl.setAttribute("fxastatus", stateValue); 1375 menuHeaderTitleEl.value = 1376 this.fluentStrings.formatValueSync(headerTitleL10nId); 1377 // If we description is empty, we hide it 1378 menuHeaderDescriptionEl.hidden = !headerDescription; 1379 menuHeaderDescriptionEl.value = headerDescription; 1380 // We remove the data-l10n-id attribute here to prevent the node's value 1381 // attribute from being overwritten by Fluent when the panel is moved 1382 // around in the DOM. 1383 menuHeaderTitleEl.removeAttribute("data-l10n-id"); 1384 menuHeaderDescriptionEl.removeAttribute("data-l10n-id"); 1385 }, 1386 1387 updateAvatarURL(mainWindowEl, avatarURL, avatarIsDefault) { 1388 if (avatarURL && !avatarIsDefault) { 1389 const bgImage = `url("${avatarURL}")`; 1390 const img = new Image(); 1391 img.onload = () => { 1392 mainWindowEl.style.setProperty("--avatar-image-url", bgImage); 1393 }; 1394 img.onerror = () => { 1395 mainWindowEl.style.removeProperty("--avatar-image-url"); 1396 }; 1397 img.src = avatarURL; 1398 } else { 1399 mainWindowEl.style.removeProperty("--avatar-image-url"); 1400 } 1401 }, 1402 1403 enableSendTabIfValidTab() { 1404 // All tabs selected must be sendable for the Send Tab button to be enabled 1405 // on the FxA menu. 1406 let canSendAllURIs = gBrowser.selectedTabs.every( 1407 t => !!BrowserUtils.getShareableURL(t.linkedBrowser.currentURI) 1408 ); 1409 1410 for (const id of [ 1411 "PanelUI-fxa-menu-sendtab-button", 1412 "PanelUI-fxa-menu-sendtab-separator", 1413 ]) { 1414 PanelMultiView.getViewNode(document, id).hidden = !canSendAllURIs; 1415 } 1416 }, 1417 1418 // This is mis-named - it can be used to record any FxA UI telemetry, whether from 1419 // the toolbar or not. The required `sourceElement` param is enough to help us know 1420 // how to record the interaction. 1421 emitFxaToolbarTelemetry(type, sourceElement) { 1422 if (UIState.isReady()) { 1423 const state = UIState.get(); 1424 const hasAvatar = state.avatarURL && !state.avatarIsDefault; 1425 let extraOptions = { 1426 fxa_status: state.status, 1427 fxa_avatar: hasAvatar ? "true" : "false", 1428 fxa_sync_on: state.syncEnabled, 1429 }; 1430 1431 let eventName = this._getEntryPointForElement(sourceElement); 1432 let category = ""; 1433 if (eventName == "fxa_avatar_menu") { 1434 category = "fxaAvatarMenu"; 1435 } else if (eventName == "fxa_app_menu") { 1436 category = "fxaAppMenu"; 1437 } else { 1438 return; 1439 } 1440 Glean[category][ 1441 "click" + 1442 type 1443 .split("_") 1444 .map(word => word[0].toUpperCase() + word.slice(1)) 1445 .join("") 1446 ]?.record(extraOptions); 1447 } 1448 }, 1449 1450 updatePanelPopup({ email, displayName, status }) { 1451 const appMenuStatus = PanelMultiView.getViewNode( 1452 document, 1453 "appMenu-fxa-status2" 1454 ); 1455 const appMenuLabel = PanelMultiView.getViewNode( 1456 document, 1457 "appMenu-fxa-label2" 1458 ); 1459 const appMenuHeaderText = PanelMultiView.getViewNode( 1460 document, 1461 "appMenu-fxa-text" 1462 ); 1463 const appMenuHeaderTitle = PanelMultiView.getViewNode( 1464 document, 1465 "appMenu-header-title" 1466 ); 1467 const appMenuHeaderDescription = PanelMultiView.getViewNode( 1468 document, 1469 "appMenu-header-description" 1470 ); 1471 const fxaPanelView = PanelMultiView.getViewNode(document, "PanelUI-fxa"); 1472 1473 let defaultLabel = this.fluentStrings.formatValueSync( 1474 "appmenu-fxa-signed-in-label" 1475 ); 1476 // Reset the status bar to its original state. 1477 appMenuLabel.setAttribute("label", defaultLabel); 1478 appMenuLabel.removeAttribute("aria-labelledby"); 1479 appMenuStatus.removeAttribute("fxastatus"); 1480 1481 if (status == UIState.STATUS_NOT_CONFIGURED) { 1482 appMenuHeaderText.hidden = false; 1483 appMenuStatus.classList.add("toolbaritem-combined-buttons"); 1484 appMenuLabel.classList.remove("subviewbutton-nav"); 1485 appMenuHeaderTitle.hidden = true; 1486 appMenuHeaderDescription.value = defaultLabel; 1487 return; 1488 } 1489 appMenuLabel.classList.remove("subviewbutton-nav"); 1490 1491 appMenuHeaderText.hidden = true; 1492 appMenuStatus.classList.remove("toolbaritem-combined-buttons"); 1493 1494 // While we prefer the display name in most case, in some strings 1495 // where the context is something like "Verify %s", the email 1496 // is used even when there's a display name. 1497 if (status == UIState.STATUS_LOGIN_FAILED) { 1498 const [tooltipDescription, errorLabel] = 1499 this.fluentStrings.formatValuesSync([ 1500 { id: "account-reconnect", args: { email } }, 1501 { id: "account-disconnected2" }, 1502 ]); 1503 appMenuStatus.setAttribute("fxastatus", "login-failed"); 1504 appMenuStatus.setAttribute("tooltiptext", tooltipDescription); 1505 appMenuLabel.classList.add("subviewbutton-nav"); 1506 appMenuHeaderTitle.hidden = false; 1507 appMenuHeaderTitle.value = errorLabel; 1508 appMenuHeaderDescription.value = displayName || email; 1509 1510 appMenuLabel.removeAttribute("label"); 1511 appMenuLabel.setAttribute( 1512 "aria-labelledby", 1513 `${appMenuHeaderTitle.id},${appMenuHeaderDescription.id}` 1514 ); 1515 return; 1516 } else if (status == UIState.STATUS_NOT_VERIFIED) { 1517 const [tooltipDescription, unverifiedLabel] = 1518 this.fluentStrings.formatValuesSync([ 1519 { id: "account-verify", args: { email } }, 1520 { id: "account-finish-account-setup" }, 1521 ]); 1522 appMenuStatus.setAttribute("fxastatus", "unverified"); 1523 appMenuStatus.setAttribute("tooltiptext", tooltipDescription); 1524 appMenuLabel.classList.add("subviewbutton-nav"); 1525 appMenuHeaderTitle.hidden = false; 1526 appMenuHeaderTitle.value = unverifiedLabel; 1527 appMenuHeaderDescription.value = email; 1528 1529 appMenuLabel.removeAttribute("label"); 1530 appMenuLabel.setAttribute( 1531 "aria-labelledby", 1532 `${appMenuHeaderTitle.id},${appMenuHeaderDescription.id}` 1533 ); 1534 return; 1535 } 1536 1537 appMenuHeaderTitle.hidden = true; 1538 appMenuHeaderDescription.value = displayName || email; 1539 appMenuStatus.setAttribute("fxastatus", "signedin"); 1540 appMenuLabel.setAttribute("label", displayName || email); 1541 appMenuLabel.classList.add("subviewbutton-nav"); 1542 fxaPanelView.setAttribute( 1543 "title", 1544 this.fluentStrings.formatValueSync("appmenu-account-header") 1545 ); 1546 appMenuStatus.removeAttribute("tooltiptext"); 1547 }, 1548 1549 updateState(state) { 1550 for (let [shown, menuId, boxId] of [ 1551 [ 1552 state.status == UIState.STATUS_NOT_CONFIGURED, 1553 "sync-setup", 1554 "PanelUI-remotetabs-setupsync", 1555 ], 1556 [ 1557 state.status == UIState.STATUS_SIGNED_IN && !state.syncEnabled, 1558 "sync-enable", 1559 "PanelUI-remotetabs-syncdisabled", 1560 ], 1561 [ 1562 state.status == UIState.STATUS_LOGIN_FAILED, 1563 "sync-reauthitem", 1564 "PanelUI-remotetabs-reauthsync", 1565 ], 1566 [ 1567 state.status == UIState.STATUS_NOT_VERIFIED, 1568 "sync-unverifieditem", 1569 "PanelUI-remotetabs-unverified", 1570 ], 1571 [ 1572 state.status == UIState.STATUS_SIGNED_IN && state.syncEnabled, 1573 "sync-syncnowitem", 1574 "PanelUI-remotetabs-main", 1575 ], 1576 ]) { 1577 document.getElementById(menuId).hidden = PanelMultiView.getViewNode( 1578 document, 1579 boxId 1580 ).hidden = !shown; 1581 } 1582 }, 1583 1584 updateSyncStatus(state) { 1585 let syncNow = 1586 document.querySelector(".syncNowBtn") || 1587 document 1588 .getElementById("appMenu-viewCache") 1589 .content.querySelector(".syncNowBtn"); 1590 const syncingUI = syncNow.getAttribute("syncstatus") == "active"; 1591 if (state.syncing != syncingUI) { 1592 // Do we need to update the UI? 1593 state.syncing ? this.onActivityStart() : this.onActivityStop(); 1594 } 1595 }, 1596 1597 async openSignInAgainPage(entryPoint) { 1598 if (!(await FxAccounts.canConnectAccount())) { 1599 return; 1600 } 1601 const url = await FxAccounts.config.promiseConnectAccountURI(entryPoint); 1602 switchToTabHavingURI(url, true, { 1603 replaceQueryString: true, 1604 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 1605 }); 1606 }, 1607 1608 async openDevicesManagementPage(entryPoint) { 1609 let url = await FxAccounts.config.promiseManageDevicesURI(entryPoint); 1610 switchToTabHavingURI(url, true, { 1611 replaceQueryString: true, 1612 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), 1613 }); 1614 }, 1615 1616 async openConnectAnotherDevice(entryPoint) { 1617 const url = await FxAccounts.config.promiseConnectDeviceURI(entryPoint); 1618 openTrustedLinkIn(url, "tab"); 1619 }, 1620 1621 async clickOpenConnectAnotherDevice(sourceElement) { 1622 this.emitFxaToolbarTelemetry("cad", sourceElement); 1623 let entryPoint = this._getEntryPointForElement(sourceElement); 1624 this.openConnectAnotherDevice(entryPoint); 1625 }, 1626 1627 openSendToDevicePromo() { 1628 const url = Services.urlFormatter.formatURLPref( 1629 "identity.sendtabpromo.url" 1630 ); 1631 switchToTabHavingURI(url, true, { replaceQueryString: true }); 1632 }, 1633 1634 async clickFxAMenuHeaderButton(sourceElement) { 1635 // Depending on the current logged in state of a user, 1636 // clicking the FxA header will either open 1637 // a sign-in page, account management page, or sync 1638 // preferences page. 1639 const { status } = UIState.get(); 1640 switch (status) { 1641 case UIState.STATUS_NOT_CONFIGURED: 1642 this.openFxAEmailFirstPageFromFxaMenu(sourceElement); 1643 break; 1644 case UIState.STATUS_LOGIN_FAILED: 1645 this.openPrefsFromFxaMenu("sync_settings", sourceElement); 1646 break; 1647 case UIState.STATUS_NOT_VERIFIED: 1648 this.openFxAEmailFirstPage("fxa_app_menu_reverify"); 1649 break; 1650 case UIState.STATUS_SIGNED_IN: 1651 this._openFxAManagePageFromElement(sourceElement); 1652 } 1653 }, 1654 1655 // Gets the telemetry "entry point" we should use for a given UI element. 1656 // This entry-point is recorded in both client telemetry (typically called the "object") 1657 // and where applicable, also communicated to the server for server telemetry via a URL query param. 1658 // 1659 // It inspects the parent elements to determine if the element is within one of our "well known" 1660 // UI groups, in which case it will return a string for that group (eg, "fxa_app_menu", "fxa_toolbar_button"). 1661 // Otherwise (eg, the item might be directly on the context menu), it will return "fxa_discoverability_native". 1662 _getEntryPointForElement(sourceElement) { 1663 // Note that when an element is in either the app menu or the toolbar button menu, 1664 // in both cases it *will* have a parent with ID "PanelUI-fxa-menu". But when 1665 // in the app menu, it will also have a grand-parent with ID "appMenu-popup". 1666 // So we must check for that outer grandparent first. 1667 const appMenuPanel = document.getElementById("appMenu-popup"); 1668 if (appMenuPanel.contains(sourceElement)) { 1669 return "fxa_app_menu"; 1670 } 1671 // If it *is* the toolbar button... 1672 if (sourceElement.id == "fxa-toolbar-menu-button") { 1673 return "fxa_avatar_menu"; 1674 } 1675 // ... or is in the panel shown by that button. 1676 const fxaMenu = document.getElementById("PanelUI-fxa-menu"); 1677 if (fxaMenu && fxaMenu.contains(sourceElement)) { 1678 return "fxa_avatar_menu"; 1679 } 1680 return "fxa_discoverability_native"; 1681 }, 1682 1683 async openFxAEmailFirstPage(entryPoint, extraParams = {}) { 1684 if (!(await FxAccounts.canConnectAccount())) { 1685 return; 1686 } 1687 const url = await FxAccounts.config.promiseConnectAccountURI( 1688 entryPoint, 1689 extraParams 1690 ); 1691 switchToTabHavingURI(url, true, { replaceQueryString: true }); 1692 }, 1693 1694 async openFxAEmailFirstPageFromFxaMenu(sourceElement, extraParams = {}) { 1695 this.emitFxaToolbarTelemetry("login", sourceElement); 1696 this.openFxAEmailFirstPage( 1697 this._getEntryPointForElement(sourceElement), 1698 extraParams 1699 ); 1700 }, 1701 1702 async openFxAManagePage(entryPoint) { 1703 const url = await FxAccounts.config.promiseManageURI(entryPoint); 1704 switchToTabHavingURI(url, true, { replaceQueryString: true }); 1705 }, 1706 1707 async _openFxAManagePageFromElement(sourceElement) { 1708 this.emitFxaToolbarTelemetry("account_settings", sourceElement); 1709 this.openFxAManagePage(this._getEntryPointForElement(sourceElement)); 1710 }, 1711 1712 // Returns true if we managed to send the tab to any targets, false otherwise. 1713 async sendTabToDevice(url, targets, title) { 1714 const fxaCommandsDevices = []; 1715 for (const target of targets) { 1716 if (fxAccounts.commands.sendTab.isDeviceCompatible(target)) { 1717 fxaCommandsDevices.push(target); 1718 } else { 1719 this.log.error(`Target ${target.id} unsuitable for send tab.`); 1720 } 1721 } 1722 // If a primary-password is enabled then it must be unlocked so FxA can get 1723 // the encryption keys from the login manager. (If we end up using the "sync" 1724 // fallback that would end up prompting by itself, but the FxA command route 1725 // will not) - so force that here. 1726 let cryptoSDR = Cc["@mozilla.org/login-manager/crypto/SDR;1"].getService( 1727 Ci.nsILoginManagerCrypto 1728 ); 1729 if (!cryptoSDR.isLoggedIn) { 1730 if (cryptoSDR.uiBusy) { 1731 this.log.info("Master password UI is busy - not sending the tabs"); 1732 return false; 1733 } 1734 try { 1735 cryptoSDR.encrypt("bacon"); // forces the mp prompt. 1736 } catch (e) { 1737 this.log.info( 1738 "Master password remains unlocked - not sending the tabs" 1739 ); 1740 return false; 1741 } 1742 } 1743 let numFailed = 0; 1744 if (fxaCommandsDevices.length) { 1745 this.log.info( 1746 `Sending a tab to ${fxaCommandsDevices 1747 .map(d => d.id) 1748 .join(", ")} using FxA commands.` 1749 ); 1750 const report = await fxAccounts.commands.sendTab.send( 1751 fxaCommandsDevices, 1752 { url, title } 1753 ); 1754 for (let { device, error } of report.failed) { 1755 this.log.error( 1756 `Failed to send a tab with FxA commands for ${device.id}.`, 1757 error 1758 ); 1759 numFailed++; 1760 } 1761 } 1762 return numFailed < targets.length; // Good enough. 1763 }, 1764 1765 populateSendTabToDevicesMenu( 1766 devicesPopup, 1767 uri, 1768 title, 1769 multiselected, 1770 createDeviceNodeFn, 1771 isFxaMenu = false 1772 ) { 1773 uri = BrowserUtils.getShareableURL(uri); 1774 if (!uri) { 1775 // log an error as everyone should have already checked this. 1776 this.log.error("Ignoring request to share a non-sharable URL"); 1777 return; 1778 } 1779 if (!createDeviceNodeFn) { 1780 createDeviceNodeFn = (targetId, name) => { 1781 let eltName = name ? "menuitem" : "menuseparator"; 1782 return document.createXULElement(eltName); 1783 }; 1784 } 1785 1786 // remove existing menu items 1787 for (let i = devicesPopup.children.length - 1; i >= 0; --i) { 1788 let child = devicesPopup.children[i]; 1789 if (child.classList.contains("sync-menuitem")) { 1790 child.remove(); 1791 } 1792 } 1793 1794 if (gSync.sendTabConfiguredAndLoading) { 1795 // We can only be in this case in the page action menu. 1796 return; 1797 } 1798 1799 const fragment = document.createDocumentFragment(); 1800 1801 const state = UIState.get(); 1802 if (state.status == UIState.STATUS_SIGNED_IN) { 1803 const targets = this.getSendTabTargets(); 1804 if (targets.length) { 1805 this._appendSendTabDeviceList( 1806 targets, 1807 fragment, 1808 createDeviceNodeFn, 1809 uri.spec, 1810 title, 1811 multiselected, 1812 isFxaMenu 1813 ); 1814 } else { 1815 this._appendSendTabSingleDevice(fragment, createDeviceNodeFn); 1816 } 1817 } else if ( 1818 state.status == UIState.STATUS_NOT_VERIFIED || 1819 state.status == UIState.STATUS_LOGIN_FAILED 1820 ) { 1821 this._appendSendTabVerify(fragment, createDeviceNodeFn); 1822 } else { 1823 // The only status not handled yet is STATUS_NOT_CONFIGURED, and 1824 // when we're in that state, none of the menus that call 1825 // populateSendTabToDevicesMenu are available, so entering this 1826 // state is unexpected. 1827 throw new Error( 1828 "Called populateSendTabToDevicesMenu when in STATUS_NOT_CONFIGURED " + 1829 "state." 1830 ); 1831 } 1832 1833 devicesPopup.appendChild(fragment); 1834 }, 1835 1836 _appendSendTabDeviceList( 1837 targets, 1838 fragment, 1839 createDeviceNodeFn, 1840 url, 1841 title, 1842 multiselected, 1843 isFxaMenu = false 1844 ) { 1845 let tabsToSend = multiselected 1846 ? gBrowser.selectedTabs.map(t => { 1847 return { 1848 url: t.linkedBrowser.currentURI.spec, 1849 title: t.linkedBrowser.contentTitle, 1850 }; 1851 }) 1852 : [{ url, title }]; 1853 1854 const send = to => { 1855 Promise.all( 1856 tabsToSend.map(t => 1857 // sendTabToDevice does not reject. 1858 this.sendTabToDevice(t.url, to, t.title) 1859 ) 1860 ).then(results => { 1861 // Show the Sent! confirmation if any of the sends succeeded. 1862 if (results.includes(true)) { 1863 // FxA button could be hidden with CSS since the user is logged out, 1864 // although it seems likely this would only happen in testing... 1865 let fxastatus = document.documentElement.getAttribute("fxastatus"); 1866 let anchorNode = 1867 (fxastatus && 1868 fxastatus != "not_configured" && 1869 document.getElementById("fxa-toolbar-menu-button")?.parentNode 1870 ?.id != "widget-overflow-list" && 1871 document.getElementById("fxa-toolbar-menu-button")) || 1872 document.getElementById("PanelUI-menu-button"); 1873 ConfirmationHint.show(anchorNode, "confirmation-hint-send-to-device"); 1874 } 1875 fxAccounts.flushLogFile(); 1876 }); 1877 }; 1878 const onSendAllCommand = () => { 1879 send(targets); 1880 }; 1881 const onTargetDeviceCommand = event => { 1882 const targetId = event.target.getAttribute("clientId"); 1883 const target = targets.find(t => t.id == targetId); 1884 send([target]); 1885 }; 1886 1887 function addTargetDevice(targetId, name, targetType, lastModified) { 1888 const targetDevice = createDeviceNodeFn( 1889 targetId, 1890 name, 1891 targetType, 1892 lastModified 1893 ); 1894 targetDevice.addEventListener( 1895 "command", 1896 targetId ? onTargetDeviceCommand : onSendAllCommand, 1897 true 1898 ); 1899 targetDevice.classList.add("sync-menuitem", "sendtab-target"); 1900 targetDevice.setAttribute("clientId", targetId); 1901 targetDevice.setAttribute("clientType", targetType); 1902 targetDevice.setAttribute("label", name); 1903 fragment.appendChild(targetDevice); 1904 } 1905 1906 for (let target of targets) { 1907 let type, lastModified; 1908 if (target.clientRecord) { 1909 type = Weave.Service.clientsEngine.getClientType( 1910 target.clientRecord.id 1911 ); 1912 lastModified = new Date(target.clientRecord.serverLastModified * 1000); 1913 } else { 1914 // For phones, FxA uses "mobile" and Sync clients uses "phone". 1915 type = target.type == "mobile" ? "phone" : target.type; 1916 lastModified = target.lastAccessTime 1917 ? new Date(target.lastAccessTime) 1918 : null; 1919 } 1920 addTargetDevice(target.id, target.name, type, lastModified); 1921 } 1922 1923 if (targets.length > 1) { 1924 // "Send to All Devices" menu item 1925 const separator = createDeviceNodeFn(); 1926 separator.classList.add("sync-menuitem"); 1927 fragment.appendChild(separator); 1928 const [allDevicesLabel, manageDevicesLabel] = 1929 this.fluentStrings.formatValuesSync( 1930 isFxaMenu 1931 ? ["account-send-to-all-devices", "account-manage-devices"] 1932 : [ 1933 "account-send-to-all-devices-titlecase", 1934 "account-manage-devices-titlecase", 1935 ] 1936 ); 1937 addTargetDevice("", allDevicesLabel, ""); 1938 1939 // "Manage devices" menu item 1940 // We piggyback on the createDeviceNodeFn implementation, 1941 // it's a big disgusting. 1942 const targetDevice = createDeviceNodeFn( 1943 null, 1944 manageDevicesLabel, 1945 null, 1946 null 1947 ); 1948 targetDevice.addEventListener( 1949 "command", 1950 () => gSync.openDevicesManagementPage("sendtab"), 1951 true 1952 ); 1953 targetDevice.classList.add("sync-menuitem", "sendtab-target"); 1954 targetDevice.setAttribute("label", manageDevicesLabel); 1955 fragment.appendChild(targetDevice); 1956 } 1957 }, 1958 1959 _appendSendTabSingleDevice(fragment, createDeviceNodeFn) { 1960 const [noDevices, learnMore, connectDevice] = 1961 this.fluentStrings.formatValuesSync([ 1962 "account-send-tab-to-device-singledevice-status", 1963 "account-send-tab-to-device-singledevice-learnmore", 1964 "account-send-tab-to-device-connectdevice", 1965 ]); 1966 const actions = [ 1967 { 1968 label: connectDevice, 1969 command: () => this.openConnectAnotherDevice("sendtab"), 1970 }, 1971 { label: learnMore, command: () => this.openSendToDevicePromo() }, 1972 ]; 1973 this._appendSendTabInfoItems( 1974 fragment, 1975 createDeviceNodeFn, 1976 noDevices, 1977 actions 1978 ); 1979 }, 1980 1981 _appendSendTabVerify(fragment, createDeviceNodeFn) { 1982 const [notVerified, verifyAccount] = this.fluentStrings.formatValuesSync([ 1983 "account-send-tab-to-device-verify-status", 1984 "account-send-tab-to-device-verify", 1985 ]); 1986 const actions = [ 1987 { label: verifyAccount, command: () => this.openPrefs("sendtab") }, 1988 ]; 1989 this._appendSendTabInfoItems( 1990 fragment, 1991 createDeviceNodeFn, 1992 notVerified, 1993 actions 1994 ); 1995 }, 1996 1997 _appendSendTabInfoItems(fragment, createDeviceNodeFn, statusLabel, actions) { 1998 const status = createDeviceNodeFn(null, statusLabel, null); 1999 status.setAttribute("label", statusLabel); 2000 status.setAttribute("disabled", true); 2001 status.classList.add("sync-menuitem"); 2002 fragment.appendChild(status); 2003 2004 const separator = createDeviceNodeFn(null, null, null); 2005 separator.classList.add("sync-menuitem"); 2006 fragment.appendChild(separator); 2007 2008 for (let { label, command } of actions) { 2009 const actionItem = createDeviceNodeFn(null, label, null); 2010 actionItem.addEventListener("command", command, true); 2011 actionItem.classList.add("sync-menuitem"); 2012 actionItem.setAttribute("label", label); 2013 fragment.appendChild(actionItem); 2014 } 2015 }, 2016 2017 // "Send Tab to Device" menu item 2018 updateTabContextMenu(aPopupMenu, aTargetTab) { 2019 // We may get here before initialisation. This situation 2020 // can lead to a empty label for 'Send To Device' Menu. 2021 this.init(); 2022 2023 if (!this.FXA_ENABLED) { 2024 // These items are hidden in onFxaDisabled(). No need to do anything. 2025 return; 2026 } 2027 let hasASendableURI = false; 2028 for (let tab of aTargetTab.multiselected 2029 ? gBrowser.selectedTabs 2030 : [aTargetTab]) { 2031 if (BrowserUtils.getShareableURL(tab.linkedBrowser.currentURI)) { 2032 hasASendableURI = true; 2033 break; 2034 } 2035 } 2036 const enabled = !this.sendTabConfiguredAndLoading && hasASendableURI; 2037 const hideItems = this.shouldHideSendContextMenuItems(enabled); 2038 2039 let sendTabsToDevice = document.getElementById("context_sendTabToDevice"); 2040 sendTabsToDevice.disabled = !enabled; 2041 let sendTabToDeviceSeparator = document.getElementById( 2042 "context_sendTabToDeviceSeparator" 2043 ); 2044 2045 if (hideItems || !hasASendableURI) { 2046 sendTabsToDevice.hidden = true; 2047 sendTabToDeviceSeparator.hidden = true; 2048 } else { 2049 let tabCount = aTargetTab.multiselected 2050 ? gBrowser.multiSelectedTabsCount 2051 : 1; 2052 sendTabsToDevice.setAttribute( 2053 "data-l10n-args", 2054 JSON.stringify({ tabCount }) 2055 ); 2056 sendTabsToDevice.hidden = false; 2057 sendTabToDeviceSeparator.hidden = false; 2058 } 2059 }, 2060 2061 // "Send Page to Device" and "Send Link to Device" menu items 2062 updateContentContextMenu(contextMenu) { 2063 if (!this.FXA_ENABLED) { 2064 // These items are hidden by default. No need to do anything. 2065 return false; 2066 } 2067 // showSendLink and showSendPage are mutually exclusive 2068 const showSendLink = 2069 contextMenu.onSaveableLink || contextMenu.onPlainTextLink; 2070 const showSendPage = 2071 !showSendLink && 2072 !( 2073 contextMenu.isContentSelected || 2074 contextMenu.onImage || 2075 contextMenu.onCanvas || 2076 contextMenu.onVideo || 2077 contextMenu.onAudio || 2078 contextMenu.onLink || 2079 contextMenu.onTextInput 2080 ); 2081 2082 const targetURI = showSendLink 2083 ? contextMenu.getLinkURI() 2084 : contextMenu.browser.currentURI; 2085 const enabled = 2086 !this.sendTabConfiguredAndLoading && 2087 BrowserUtils.getShareableURL(targetURI); 2088 const hideItems = this.shouldHideSendContextMenuItems(enabled); 2089 2090 contextMenu.showItem( 2091 "context-sendpagetodevice", 2092 !hideItems && showSendPage 2093 ); 2094 for (const id of [ 2095 "context-sendlinktodevice", 2096 "context-sep-sendlinktodevice", 2097 ]) { 2098 contextMenu.showItem(id, !hideItems && showSendLink); 2099 } 2100 2101 if (!showSendLink && !showSendPage) { 2102 return false; 2103 } 2104 2105 contextMenu.setItemAttr( 2106 showSendPage ? "context-sendpagetodevice" : "context-sendlinktodevice", 2107 "disabled", 2108 !enabled || null 2109 ); 2110 // return true if context menu items are visible 2111 return !hideItems && (showSendPage || showSendLink); 2112 }, 2113 2114 // Functions called by observers 2115 onActivityStart() { 2116 this._isCurrentlySyncing = true; 2117 clearTimeout(this._syncAnimationTimer); 2118 this._syncStartTime = Date.now(); 2119 2120 document.querySelectorAll(".syncnow-label").forEach(el => { 2121 let l10nId = el.getAttribute("syncing-data-l10n-id"); 2122 document.l10n.setAttributes(el, l10nId); 2123 }); 2124 2125 document.querySelectorAll(".syncNowBtn").forEach(el => { 2126 el.setAttribute("syncstatus", "active"); 2127 }); 2128 2129 document 2130 .getElementById("appMenu-viewCache") 2131 .content.querySelectorAll(".syncNowBtn") 2132 .forEach(el => { 2133 el.setAttribute("syncstatus", "active"); 2134 }); 2135 }, 2136 2137 _onActivityStop() { 2138 this._isCurrentlySyncing = false; 2139 if (!gBrowser) { 2140 return; 2141 } 2142 2143 document.querySelectorAll(".syncnow-label").forEach(el => { 2144 let l10nId = el.getAttribute("sync-now-data-l10n-id"); 2145 document.l10n.setAttributes(el, l10nId); 2146 }); 2147 2148 document.querySelectorAll(".syncNowBtn").forEach(el => { 2149 el.removeAttribute("syncstatus"); 2150 }); 2151 2152 document 2153 .getElementById("appMenu-viewCache") 2154 .content.querySelectorAll(".syncNowBtn") 2155 .forEach(el => { 2156 el.removeAttribute("syncstatus"); 2157 }); 2158 2159 Services.obs.notifyObservers(null, "test:browser-sync:activity-stop"); 2160 }, 2161 2162 onActivityStop() { 2163 let now = Date.now(); 2164 let syncDuration = now - this._syncStartTime; 2165 2166 if (syncDuration < MIN_STATUS_ANIMATION_DURATION) { 2167 let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration; 2168 clearTimeout(this._syncAnimationTimer); 2169 this._syncAnimationTimer = setTimeout( 2170 () => this._onActivityStop(), 2171 animationTime 2172 ); 2173 } else { 2174 this._onActivityStop(); 2175 } 2176 }, 2177 2178 // Disconnect from sync, and optionally disconnect from the FxA account. 2179 // Returns true if the disconnection happened (ie, if the user didn't decline 2180 // when asked to confirm) 2181 async disconnect({ confirm = true, disconnectAccount = true } = {}) { 2182 if (disconnectAccount) { 2183 let deleteLocalData = false; 2184 if (confirm) { 2185 let options = await this._confirmFxaAndSyncDisconnect(); 2186 if (!options.userConfirmedDisconnect) { 2187 return false; 2188 } 2189 deleteLocalData = options.deleteLocalData; 2190 } 2191 return this._disconnectFxaAndSync(deleteLocalData); 2192 } 2193 2194 if (confirm && !(await this._confirmSyncDisconnect())) { 2195 return false; 2196 } 2197 return this._disconnectSync(); 2198 }, 2199 2200 // Prompt the user to confirm disconnect from FxA and sync with the option 2201 // to delete syncable data from the device. 2202 async _confirmFxaAndSyncDisconnect() { 2203 let options = { 2204 userConfirmedDisconnect: false, 2205 deleteLocalData: false, 2206 }; 2207 2208 let [title, body, button, checkbox] = await document.l10n.formatValues([ 2209 { id: "fxa-signout-dialog-title2" }, 2210 { id: "fxa-signout-dialog-body" }, 2211 { id: "fxa-signout-dialog2-button" }, 2212 { id: "fxa-signout-dialog2-checkbox" }, 2213 ]); 2214 2215 const flags = 2216 Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + 2217 Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1; 2218 2219 if (!UIState.get().syncEnabled) { 2220 checkbox = null; 2221 } 2222 2223 const result = await Services.prompt.asyncConfirmEx( 2224 window.browsingContext, 2225 Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, 2226 title, 2227 body, 2228 flags, 2229 button, 2230 null, 2231 null, 2232 checkbox, 2233 false 2234 ); 2235 const propBag = result.QueryInterface(Ci.nsIPropertyBag2); 2236 options.userConfirmedDisconnect = propBag.get("buttonNumClicked") == 0; 2237 options.deleteLocalData = propBag.get("checked"); 2238 2239 return options; 2240 }, 2241 2242 async _disconnectFxaAndSync(deleteLocalData) { 2243 const { SyncDisconnect } = ChromeUtils.importESModule( 2244 "resource://services-sync/SyncDisconnect.sys.mjs" 2245 ); 2246 // Record telemetry. 2247 await fxAccounts.telemetry.recordDisconnection(null, "ui"); 2248 2249 await SyncDisconnect.disconnect(deleteLocalData).catch(e => { 2250 console.error("Failed to disconnect.", e); 2251 }); 2252 2253 // Clear the attached clients list upon successfully disconnecting 2254 this._attachedClients = null; 2255 2256 return true; 2257 }, 2258 2259 // Prompt the user to confirm disconnect from sync. In this case the data 2260 // on the device is not deleted. 2261 async _confirmSyncDisconnect() { 2262 const [title, body, button] = await document.l10n.formatValues([ 2263 { id: `sync-disconnect-dialog-title2` }, 2264 { id: `sync-disconnect-dialog-body` }, 2265 { id: "sync-disconnect-dialog-button" }, 2266 ]); 2267 2268 const flags = 2269 Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 + 2270 Services.prompt.BUTTON_TITLE_CANCEL * Services.prompt.BUTTON_POS_1; 2271 2272 // buttonPressed will be 0 for disconnect, 1 for cancel. 2273 const buttonPressed = Services.prompt.confirmEx( 2274 window, 2275 title, 2276 body, 2277 flags, 2278 button, 2279 null, 2280 null, 2281 null, 2282 {} 2283 ); 2284 return buttonPressed == 0; 2285 }, 2286 2287 async _disconnectSync() { 2288 await fxAccounts.telemetry.recordDisconnection("sync", "ui"); 2289 2290 await Weave.Service.promiseInitialized; 2291 await Weave.Service.startOver(); 2292 2293 return true; 2294 }, 2295 2296 // doSync forces a sync - it *does not* return a promise as it is called 2297 // via the various UI components. 2298 doSync() { 2299 if (!UIState.isReady()) { 2300 return; 2301 } 2302 // Note we don't bother checking if sync is actually enabled - none of the 2303 // UI which calls this function should be visible in that case. 2304 const state = UIState.get(); 2305 if (state.status == UIState.STATUS_SIGNED_IN) { 2306 this.updateSyncStatus({ syncing: true }); 2307 Services.tm.dispatchToMainThread(() => { 2308 // We are pretty confident that push helps us pick up all FxA commands, 2309 // but some users might have issues with push, so let's unblock them 2310 // by fetching the missed FxA commands on manual sync. 2311 fxAccounts.commands.pollDeviceCommands().catch(e => { 2312 this.log.error("Fetching missed remote commands failed.", e); 2313 }); 2314 Weave.Service.sync(); 2315 }); 2316 } 2317 }, 2318 2319 doSyncFromFxaMenu(sourceElement) { 2320 this.doSync(); 2321 this.emitFxaToolbarTelemetry("sync_now", sourceElement); 2322 }, 2323 2324 openPrefs(entryPoint = "syncbutton", origin = undefined, urlParams = {}) { 2325 window.openPreferences("paneSync", { 2326 origin, 2327 urlParams: { ...urlParams, entrypoint: entryPoint }, 2328 }); 2329 }, 2330 2331 openPrefsFromFxaMenu(type, sourceElement) { 2332 this.emitFxaToolbarTelemetry(type, sourceElement); 2333 let entryPoint = this._getEntryPointForElement(sourceElement); 2334 this.openPrefs(entryPoint); 2335 }, 2336 2337 openChooseWhatToSync(type, sourceElement) { 2338 this.emitFxaToolbarTelemetry(type, sourceElement); 2339 let entryPoint = this._getEntryPointForElement(sourceElement); 2340 this.openPrefs(entryPoint, null, { action: "choose-what-to-sync" }); 2341 }, 2342 2343 /** 2344 * Opens the appropriate sync setup flow based on whether the user has sync keys. 2345 * - If the user has sync keys: opens sync preferences to configure what to sync 2346 * - If the user doesn't have sync keys (third-party auth): opens FxA to create password 2347 */ 2348 async openSyncSetup(type, sourceElement, extraParams = {}) { 2349 this.emitFxaToolbarTelemetry(type, sourceElement); 2350 const entryPoint = this._getEntryPointForElement(sourceElement); 2351 2352 try { 2353 // Check if the user has sync keys 2354 const hasKeys = await fxAccounts.keys.hasKeysForScope(SCOPE_APP_SYNC); 2355 2356 if (hasKeys) { 2357 // User has keys - go to prefs to configure what to sync 2358 this.openPrefs(entryPoint, null, { action: "choose-what-to-sync" }); 2359 } else { 2360 // User doesn't have keys (third-party auth) - go to FxA to create password 2361 // This will request SCOPE_APP_SYNC so FxA knows to generate sync keys 2362 if (!(await FxAccounts.canConnectAccount())) { 2363 return; 2364 } 2365 const url = await FxAccounts.config.promiseSetPasswordURI( 2366 entryPoint, 2367 extraParams 2368 ); 2369 switchToTabHavingURI(url, true, { replaceQueryString: true }); 2370 } 2371 } catch (err) { 2372 this.log.error("Failed to determine sync setup flow", err); 2373 // Fall back to opening prefs 2374 this.openPrefs(entryPoint); 2375 } 2376 }, 2377 2378 openSyncedTabsPanel() { 2379 let placement = CustomizableUI.getPlacementOfWidget("sync-button"); 2380 let area = placement?.area; 2381 let anchor = document.getElementById("sync-button"); 2382 if (area == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) { 2383 // The button is in the overflow panel, so we need to show the panel, 2384 // then show our subview. 2385 let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); 2386 navbar.overflowable.show().then(() => { 2387 PanelUI.showSubView("PanelUI-remotetabs", anchor); 2388 }, console.error); 2389 } else { 2390 if ( 2391 !anchor?.checkVisibility({ checkVisibilityCSS: true, flush: false }) 2392 ) { 2393 anchor = document.getElementById("PanelUI-menu-button"); 2394 } 2395 // It is placed somewhere else - just try and show it. 2396 PanelUI.showSubView("PanelUI-remotetabs", anchor); 2397 } 2398 }, 2399 2400 refreshSyncButtonsTooltip() { 2401 const state = UIState.get(); 2402 this.updateSyncButtonsTooltip(state); 2403 }, 2404 2405 /* Update the tooltip for the sync icon in the main menu and in Synced Tabs. 2406 If Sync is configured, the tooltip is when the last sync occurred, 2407 otherwise the tooltip reflects the fact that Sync needs to be 2408 (re-)configured. 2409 */ 2410 updateSyncButtonsTooltip(state) { 2411 // Sync buttons are 1/2 Sync related and 1/2 FxA related 2412 let l10nId, l10nArgs; 2413 switch (state.status) { 2414 case UIState.STATUS_NOT_VERIFIED: 2415 // "needs verification" 2416 l10nId = "account-verify"; 2417 l10nArgs = { email: state.email }; 2418 break; 2419 case UIState.STATUS_LOGIN_FAILED: 2420 // "need to reconnect/re-enter your password" 2421 l10nId = "account-reconnect"; 2422 l10nArgs = { email: state.email }; 2423 break; 2424 case UIState.STATUS_NOT_CONFIGURED: 2425 // Button is not shown in this state 2426 break; 2427 default: { 2428 // Sync appears configured - format the "last synced at" time. 2429 let lastSyncDate = this.formatLastSyncDate(state.lastSync); 2430 if (lastSyncDate) { 2431 l10nId = "appmenu-fxa-last-sync"; 2432 l10nArgs = { time: lastSyncDate }; 2433 } 2434 } 2435 } 2436 const tooltiptext = l10nId 2437 ? this.fluentStrings.formatValueSync(l10nId, l10nArgs) 2438 : null; 2439 2440 let syncNowBtns = [ 2441 "PanelUI-remotetabs-syncnow", 2442 "PanelUI-fxa-menu-syncnow-button", 2443 ]; 2444 syncNowBtns.forEach(id => { 2445 let el = PanelMultiView.getViewNode(document, id); 2446 if (tooltiptext) { 2447 el.setAttribute("tooltiptext", tooltiptext); 2448 } else { 2449 el.removeAttribute("tooltiptext"); 2450 } 2451 }); 2452 }, 2453 2454 get relativeTimeFormat() { 2455 delete this.relativeTimeFormat; 2456 return (this.relativeTimeFormat = new Services.intl.RelativeTimeFormat( 2457 undefined, 2458 { style: "long" } 2459 )); 2460 }, 2461 2462 formatLastSyncDate(date) { 2463 if (!date) { 2464 // Date can be null before the first sync! 2465 return null; 2466 } 2467 try { 2468 let adjustedDate = new Date(Date.now() - 1000); 2469 let relativeDateStr = this.relativeTimeFormat.formatBestUnit( 2470 date < adjustedDate ? date : adjustedDate 2471 ); 2472 return relativeDateStr; 2473 } catch (ex) { 2474 // shouldn't happen, but one client having an invalid date shouldn't 2475 // break the entire feature. 2476 this.log.warn("failed to format lastSync time", date, ex); 2477 return null; 2478 } 2479 }, 2480 2481 onClientsSynced() { 2482 // Note that this element is only shown if Sync is enabled. 2483 let element = PanelMultiView.getViewNode( 2484 document, 2485 "PanelUI-remotetabs-main" 2486 ); 2487 if (element) { 2488 if (Weave.Service.clientsEngine.stats.numClients > 1) { 2489 element.setAttribute("devices-status", "multi"); 2490 } else { 2491 element.setAttribute("devices-status", "single"); 2492 } 2493 } 2494 }, 2495 2496 onFxaDisabled() { 2497 document.documentElement.setAttribute("fxadisabled", true); 2498 2499 const toHide = [...document.querySelectorAll(".sync-ui-item")]; 2500 for (const item of toHide) { 2501 item.hidden = true; 2502 } 2503 }, 2504 2505 /** 2506 * Checks if the current list of attached clients to the Mozilla account 2507 * has a service associated with the passed in Id 2508 * 2509 * @param {string} clientId 2510 * A known static Id from FxA that identifies the service it's associated with 2511 * @returns {boolean} 2512 * Returns true/false whether the current account has the associated client 2513 */ 2514 hasClientForId(clientId) { 2515 return this._attachedClients?.some(c => !!c.id && c.id === clientId); 2516 }, 2517 2518 updateCTAPanel(anchor) { 2519 const mainPanelEl = PanelMultiView.getViewNode( 2520 document, 2521 "PanelUI-fxa-cta-menu" 2522 ); 2523 2524 // If we're not in the experiment or in the app menu (hamburger) 2525 // do not show this CTA panel 2526 if ( 2527 !this.FXA_CTA_MENU_ENABLED || 2528 (anchor && anchor.id === "appMenu-fxa-label2") 2529 ) { 2530 // If we've previously shown this but got disabled 2531 // we should ensure we hide the panel 2532 mainPanelEl.hidden = true; 2533 return; 2534 } 2535 2536 // Monitor checks 2537 let monitorPanelEl = PanelMultiView.getViewNode( 2538 document, 2539 "PanelUI-fxa-menu-monitor-button" 2540 ); 2541 let monitorEnabled = Services.prefs.getBoolPref( 2542 "identity.fxaccounts.toolbar.pxiToolbarEnabled.monitorEnabled", 2543 false 2544 ); 2545 monitorPanelEl.hidden = !monitorEnabled; 2546 2547 // Relay checks 2548 let relayPanelEl = PanelMultiView.getViewNode( 2549 document, 2550 "PanelUI-fxa-menu-relay-button" 2551 ); 2552 let relayEnabled = 2553 BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.RELAY) && 2554 Services.prefs.getBoolPref( 2555 "identity.fxaccounts.toolbar.pxiToolbarEnabled.relayEnabled", 2556 false 2557 ); 2558 let myServicesRelayPanelEl = PanelMultiView.getViewNode( 2559 document, 2560 "PanelUI-services-menu-relay-button" 2561 ); 2562 let servicesContainerEl = PanelMultiView.getViewNode( 2563 document, 2564 "PanelUI-fxa-menu-services" 2565 ); 2566 if (this.isSignedIn) { 2567 const hasRelayClient = this.hasClientForId(FX_RELAY_OAUTH_CLIENT_ID); 2568 relayPanelEl.hidden = hasRelayClient; 2569 // Right now only relay is under "my services" so if we don't have, we turn it off 2570 myServicesRelayPanelEl.hidden = !hasRelayClient; 2571 servicesContainerEl.hidden = !hasRelayClient; 2572 } else { 2573 relayPanelEl.hidden = !relayEnabled; 2574 // We'll never show my services when signed out 2575 myServicesRelayPanelEl.hidden = true; 2576 servicesContainerEl.hidden = true; 2577 } 2578 2579 // VPN checks 2580 let VpnPanelEl = PanelMultiView.getViewNode( 2581 document, 2582 "PanelUI-fxa-menu-vpn-button" 2583 ); 2584 let vpnEnabled = 2585 BrowserUtils.shouldShowPromo(BrowserUtils.PromoType.VPN) && 2586 Services.prefs.getBoolPref( 2587 "identity.fxaccounts.toolbar.pxiToolbarEnabled.vpnEnabled", 2588 false 2589 ); 2590 VpnPanelEl.hidden = !vpnEnabled; 2591 2592 // We should only the show the separator if we have at least one CTA enabled 2593 PanelMultiView.getViewNode(document, "PanelUI-products-separator").hidden = 2594 !monitorEnabled && !relayEnabled && !vpnEnabled; 2595 mainPanelEl.hidden = false; 2596 }, 2597 2598 async openMonitorLink(sourceElement) { 2599 this.emitFxaToolbarTelemetry("monitor_cta", sourceElement); 2600 await this.openCtaLink( 2601 FX_MONITOR_OAUTH_CLIENT_ID, 2602 new URL("https://monitor.firefox.com"), 2603 new URL("https://monitor.firefox.com/user/breaches") 2604 ); 2605 }, 2606 2607 async openRelayLink(sourceElement) { 2608 this.emitFxaToolbarTelemetry("relay_cta", sourceElement); 2609 await this.openCtaLink( 2610 FX_RELAY_OAUTH_CLIENT_ID, 2611 new URL("https://relay.firefox.com"), 2612 new URL("https://relay.firefox.com/accounts/profile") 2613 ); 2614 }, 2615 2616 async openVPNLink(sourceElement) { 2617 this.emitFxaToolbarTelemetry("vpn_cta", sourceElement); 2618 await this.openCtaLink( 2619 VPN_OAUTH_CLIENT_ID, 2620 new URL("https://www.mozilla.org/en-US/products/vpn/"), 2621 new URL("https://www.mozilla.org/en-US/products/vpn/") 2622 ); 2623 }, 2624 2625 // A generic opening based on 2626 async openCtaLink(clientId, defaultUrl, signedInUrl) { 2627 const params = { 2628 utm_medium: "firefox-desktop", 2629 utm_source: "toolbar", 2630 utm_campaign: "discovery", 2631 }; 2632 const searchParams = new URLSearchParams(params); 2633 2634 if (!this.isSignedIn) { 2635 // Add the base params + not signed in 2636 defaultUrl.search = searchParams.toString(); 2637 defaultUrl.searchParams.append("utm_content", "notsignedin"); 2638 this.openLink(defaultUrl); 2639 PanelUI.hide(); 2640 return; 2641 } 2642 2643 const url = this.hasClientForId(clientId) ? signedInUrl : defaultUrl; 2644 // Add base params + signed in 2645 url.search = searchParams.toString(); 2646 url.searchParams.append("utm_content", "signedIn"); 2647 2648 this.openLink(url); 2649 PanelUI.hide(); 2650 }, 2651 2652 /** 2653 * Returns any experimental copy that we want to try for FxA sign-in CTAs in 2654 * the event that the user is enrolled in an experiment. 2655 * 2656 * The only ctaCopyVariant's that are expected are: 2657 * 2658 * - control 2659 * - sync-devices 2660 * - backup-data 2661 * - backup-sync 2662 * - mobile 2663 * 2664 * If "control" is set, `null` is returned to indicate default strings, 2665 * but impressions will still be recorded. 2666 * 2667 * @param {NimbusFeature} feature 2668 * One of either NimbusFeatures.fxaAppMenuItem or 2669 * NimbusFeatures.fxaAvatarMenuItem. 2670 * @returns {object|string|null} 2671 * If feature is NimbusFeatures.fxaAppMenuItem, this will return the Fluent 2672 * string ID for the App Menu CTA to appear for users to sign in. 2673 * 2674 * If feature is NimbusFeatures.fxaAvatarMenuItem, this will return an 2675 * object with two properties: 2676 * 2677 * headerTitleL10nId (string): 2678 * The Fluent ID for the header string for the avatar menu CTA. 2679 * headerDescription (string): 2680 * The raw string for the description for the avatar menu CTA. 2681 * 2682 * If there is no copy variant being tested, this will return null. 2683 */ 2684 getMenuCtaCopy(feature) { 2685 const ctaCopyVariant = feature.getVariable("ctaCopyVariant"); 2686 let headerTitleL10nId; 2687 let headerDescription; 2688 switch (ctaCopyVariant) { 2689 case "sync-devices": { 2690 if (feature === NimbusFeatures.fxaAppMenuItem) { 2691 return "fxa-menu-message-sync-devices-collapsed-text"; 2692 } 2693 headerTitleL10nId = "fxa-menu-message-sync-devices-primary-text"; 2694 headerDescription = this.fluentStrings.formatValueSync( 2695 "fxa-menu-message-sync-devices-secondary-text" 2696 ); 2697 break; 2698 } 2699 case "backup-data": { 2700 if (feature === NimbusFeatures.fxaAppMenuItem) { 2701 return "fxa-menu-message-backup-data-collapsed-text"; 2702 } 2703 headerTitleL10nId = "fxa-menu-message-backup-data-primary-text"; 2704 headerDescription = this.fluentStrings.formatValueSync( 2705 "fxa-menu-message-backup-data-secondary-text" 2706 ); 2707 break; 2708 } 2709 case "backup-sync": { 2710 if (feature === NimbusFeatures.fxaAppMenuItem) { 2711 return "fxa-menu-message-backup-sync-collapsed-text"; 2712 } 2713 headerTitleL10nId = "fxa-menu-message-backup-sync-primary-text"; 2714 headerDescription = this.fluentStrings.formatValueSync( 2715 "fxa-menu-message-backup-sync-secondary-text" 2716 ); 2717 break; 2718 } 2719 case "mobile": { 2720 if (feature === NimbusFeatures.fxaAppMenuItem) { 2721 return "fxa-menu-message-mobile-collapsed-text"; 2722 } 2723 headerTitleL10nId = "fxa-menu-message-mobile-primary-text"; 2724 headerDescription = this.fluentStrings.formatValueSync( 2725 "fxa-menu-message-mobile-secondary-text" 2726 ); 2727 break; 2728 } 2729 default: { 2730 return null; 2731 } 2732 } 2733 2734 return { headerTitleL10nId, headerDescription }; 2735 }, 2736 2737 /** 2738 * Updates the FxA button to show the right avatar variant in the event that 2739 * this client is not currently signed into an account. 2740 * 2741 * @param {string} variant 2742 * One of the string constants for the avatarIconVariant variable on the 2743 * fxaButtonVisibility feature. 2744 */ 2745 applyAvatarIconVariant(variant) { 2746 const ICON_VARIANTS = ["control", "human-circle", "fox-circle"]; 2747 2748 if (!ICON_VARIANTS.includes(variant)) { 2749 return; 2750 } 2751 2752 document.documentElement.setAttribute("fxa-avatar-icon-variant", variant); 2753 }, 2754 2755 openLink(url) { 2756 switchToTabHavingURI(url, true, { replaceQueryString: true }); 2757 }, 2758 2759 QueryInterface: ChromeUtils.generateQI([ 2760 "nsIObserver", 2761 "nsISupportsWeakReference", 2762 ]), 2763 };