/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ var gFxAccounts = { PREF_SYNC_START_DOORHANGER: "services.sync.ui.showSyncStartDoorhanger", DOORHANGER_ACTIVATE_DELAY_MS: 5000, SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration", _initialized: false, _inCustomizationMode: false, get weave() { delete this.weave; return this.weave = Cc["@mozilla.org/weave/service;1"] .getService(Ci.nsISupports) .wrappedJSObject; }, get topics() { // Do all this dance to lazy-load FxAccountsCommon. delete this.topics; return this.topics = [ "weave:service:ready", "weave:service:sync:start", "weave:service:login:error", "weave:service:setup-complete", "weave:ui:login:error", "fxa-migration:state-changed", this.FxAccountsCommon.ONLOGIN_NOTIFICATION, this.FxAccountsCommon.ONVERIFIED_NOTIFICATION, this.FxAccountsCommon.ONLOGOUT_NOTIFICATION, this.FxAccountsCommon.ON_PROFILE_CHANGE_NOTIFICATION, ]; }, get panelUIFooter() { delete this.panelUIFooter; return this.panelUIFooter = document.getElementById("PanelUI-footer-fxa"); }, get panelUIStatus() { delete this.panelUIStatus; return this.panelUIStatus = document.getElementById("PanelUI-fxa-status"); }, get panelUIAvatar() { delete this.panelUIAvatar; return this.panelUIAvatar = document.getElementById("PanelUI-fxa-avatar"); }, get panelUILabel() { delete this.panelUILabel; return this.panelUILabel = document.getElementById("PanelUI-fxa-label"); }, get panelUIIcon() { delete this.panelUIIcon; return this.panelUIIcon = document.getElementById("PanelUI-fxa-icon"); }, get strings() { delete this.strings; return this.strings = Services.strings.createBundle( "chrome://browser/locale/accounts.properties" ); }, get loginFailed() { // Referencing Weave.Service will implicitly initialize sync, and we don't // want to force that - so first check if it is ready. let service = Cc["@mozilla.org/weave/service;1"] .getService(Components.interfaces.nsISupports) .wrappedJSObject; if (!service.ready) { return false; } // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in". // All other login failures are assumed to be transient and should go // away by themselves, so aren't reflected here. return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED; }, get isActiveWindow() { let fm = Services.focus; return fm.activeWindow == window; }, init: function () { // Bail out if we're already initialized and for pop-up windows. if (this._initialized || !window.toolbar.visible) { return; } for (let topic of this.topics) { Services.obs.addObserver(this, topic, false); } addEventListener("activate", this); gNavToolbox.addEventListener("customizationstarting", this); gNavToolbox.addEventListener("customizationending", this); EnsureFxAccountsWebChannel(); this._initialized = true; this.updateUI(); }, uninit: function () { if (!this._initialized) { return; } for (let topic of this.topics) { Services.obs.removeObserver(this, topic); } this._initialized = false; }, observe: function (subject, topic, data) { switch (topic) { case this.FxAccountsCommon.ONVERIFIED_NOTIFICATION: Services.prefs.setBoolPref(this.PREF_SYNC_START_DOORHANGER, true); break; case "weave:service:sync:start": this.onSyncStart(); break; case "fxa-migration:state-changed": this.onMigrationStateChanged(data, subject); break; case this.FxAccountsCommon.ONPROFILE_IMAGE_CHANGE_NOTIFICATION: this.updateUI(); break; default: this.updateUI(); break; } }, onSyncStart: function () { if (!this.isActiveWindow) { return; } let showDoorhanger = false; try { showDoorhanger = Services.prefs.getBoolPref(this.PREF_SYNC_START_DOORHANGER); } catch (e) { /* The pref might not exist. */ } if (showDoorhanger) { Services.prefs.clearUserPref(this.PREF_SYNC_START_DOORHANGER); this.showSyncStartedDoorhanger(); } }, onMigrationStateChanged: function () { // Since we nuked most of the migration code, this notification will fire // once after legacy Sync has been disconnected (and should never fire // again) let nb = window.document.getElementById("global-notificationbox"); let msg = this.strings.GetStringFromName("autoDisconnectDescription") let signInLabel = this.strings.GetStringFromName("autoDisconnectSignIn.label"); let signInAccessKey = this.strings.GetStringFromName("autoDisconnectSignIn.accessKey"); let learnMoreLink = this.fxaMigrator.learnMoreLink; let buttons = [ { label: signInLabel, accessKey: signInAccessKey, callback: () => { this.openPreferences(); } } ]; let fragment = document.createDocumentFragment(); let msgNode = document.createTextNode(msg); fragment.appendChild(msgNode); if (learnMoreLink) { let link = document.createElement("label"); link.className = "text-link"; link.setAttribute("value", learnMoreLink.text); link.href = learnMoreLink.href; fragment.appendChild(link); } nb.appendNotification(fragment, this.SYNC_MIGRATION_NOTIFICATION_TITLE, undefined, nb.PRIORITY_WARNING_LOW, buttons); // ensure the hamburger menu reflects the newly disconnected state. this.updateAppMenuItem(); }, handleEvent: function (event) { if (event.type == "activate") { // Our window might have been in the background while we received the // sync:start notification. If still needed, show the doorhanger after // a short delay. Without this delay the doorhanger would not show up // or with a too small delay show up while we're still animating the // window. setTimeout(() => this.onSyncStart(), this.DOORHANGER_ACTIVATE_DELAY_MS); } else { this._inCustomizationMode = event.type == "customizationstarting"; this.updateAppMenuItem(); } }, showDoorhanger: function (id) { let panel = document.getElementById(id); let anchor = document.getElementById("PanelUI-menu-button"); let iconAnchor = document.getAnonymousElementByAttribute(anchor, "class", "toolbarbutton-icon"); panel.hidden = false; panel.openPopup(iconAnchor || anchor, "bottomcenter topright"); }, showSyncStartedDoorhanger: function () { this.showDoorhanger("sync-start-panel"); }, updateUI: function () { // It's possible someone signed in to FxA after seeing our notification // about "Legacy Sync migration" (which now is actually "Legacy Sync // auto-disconnect") so kill that notification if it still exists. let nb = window.document.getElementById("global-notificationbox"); let n = nb.getNotificationWithValue(this.SYNC_MIGRATION_NOTIFICATION_TITLE); if (n) { nb.removeNotification(n, true); } this.updateAppMenuItem(); }, // Note that updateAppMenuItem() returns a Promise that's only used by tests. updateAppMenuItem: function () { let profileInfoEnabled = false; try { profileInfoEnabled = Services.prefs.getBoolPref("identity.fxaccounts.profile_image.enabled"); } catch (e) { } // Bail out if FxA is disabled. if (!this.weave.fxAccountsEnabled) { return Promise.resolve(); } this.panelUIFooter.hidden = false; // Make sure the button is disabled in customization mode. if (this._inCustomizationMode) { this.panelUIStatus.setAttribute("disabled", "true"); this.panelUILabel.setAttribute("disabled", "true"); this.panelUIAvatar.setAttribute("disabled", "true"); this.panelUIIcon.setAttribute("disabled", "true"); } else { this.panelUIStatus.removeAttribute("disabled"); this.panelUILabel.removeAttribute("disabled"); this.panelUIAvatar.removeAttribute("disabled"); this.panelUIIcon.removeAttribute("disabled"); } let defaultLabel = this.panelUIStatus.getAttribute("defaultlabel"); let errorLabel = this.panelUIStatus.getAttribute("errorlabel"); let unverifiedLabel = this.panelUIStatus.getAttribute("unverifiedlabel"); let signedInTooltiptext = this.panelUIStatus.getAttribute("signedinTooltiptext"); let updateWithUserData = (userData) => { // Window might have been closed while fetching data. if (window.closed) { return; } // Reset the button to its original state. this.panelUILabel.setAttribute("label", defaultLabel); this.panelUIStatus.removeAttribute("tooltiptext"); this.panelUIFooter.removeAttribute("fxastatus"); this.panelUIFooter.removeAttribute("fxaprofileimage"); this.panelUIAvatar.style.removeProperty("list-style-image"); let showErrorBadge = false; if (!this._inCustomizationMode && userData) { // At this point we consider the user as logged-in (but still can be in an error state) if (this.loginFailed) { let tooltipDescription = this.strings.formatStringFromName("reconnectDescription", [userData.email], 1); this.panelUIFooter.setAttribute("fxastatus", "error"); this.panelUILabel.setAttribute("label", errorLabel); this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription); showErrorBadge = true; } else if (!userData.verified) { let tooltipDescription = this.strings.formatStringFromName("verifyDescription", [userData.email], 1); this.panelUIFooter.setAttribute("fxastatus", "error"); this.panelUIFooter.setAttribute("unverified", "true"); this.panelUILabel.setAttribute("label", unverifiedLabel); this.panelUIStatus.setAttribute("tooltiptext", tooltipDescription); showErrorBadge = true; } else { this.panelUIFooter.setAttribute("fxastatus", "signedin"); this.panelUILabel.setAttribute("label", userData.email); this.panelUIStatus.setAttribute("tooltiptext", signedInTooltiptext); } if (profileInfoEnabled) { this.panelUIFooter.setAttribute("fxaprofileimage", "enabled"); } } if (showErrorBadge) { gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication"); } else { gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA); } } let updateWithProfile = (profile) => { if (!this._inCustomizationMode && profileInfoEnabled) { if (profile.displayName) { this.panelUILabel.setAttribute("label", profile.displayName); } if (profile.avatar) { this.panelUIFooter.setAttribute("fxaprofileimage", "set"); let bgImage = "url(\"" + profile.avatar + "\")"; this.panelUIAvatar.style.listStyleImage = bgImage; let img = new Image(); img.onerror = () => { // Clear the image if it has trouble loading. Since this callback is asynchronous // we check to make sure the image is still the same before we clear it. if (this.panelUIAvatar.style.listStyleImage === bgImage) { this.panelUIFooter.removeAttribute("fxaprofileimage"); this.panelUIAvatar.style.removeProperty("list-style-image"); } }; img.src = profile.avatar; } } } return fxAccounts.getSignedInUser().then(userData => { // userData may be null here when the user is not signed-in, but that's expected updateWithUserData(userData); // unverified users cause us to spew log errors fetching an OAuth token // to fetch the profile, so don't even try in that case. if (!userData || !userData.verified || !profileInfoEnabled) { return null; // don't even try to grab the profile. } return fxAccounts.getSignedInUserProfile().catch(err => { // Not fetching the profile is sad but the FxA logs will already have noise. return null; }); }).then(profile => { if (!profile) { return; } updateWithProfile(profile); }).catch(error => { // This is most likely in tests, were we quickly log users in and out. // The most likely scenario is a user logged out, so reflect that. // Bug 995134 calls for better errors so we could retry if we were // sure this was the failure reason. this.FxAccountsCommon.log.error("Error updating FxA account info", error); updateWithUserData(null); }); }, onMenuPanelCommand: function () { switch (this.panelUIFooter.getAttribute("fxastatus")) { case "signedin": this.openPreferences(); break; case "error": if (this.panelUIFooter.getAttribute("unverified")) { this.openPreferences(); } else { this.openSignInAgainPage("menupanel"); } break; default: this.openPreferences(); break; } PanelUI.hide(); }, openPreferences: function () { openPreferences("paneSync", { urlParams: { entrypoint: "menupanel" } }); }, openAccountsPage: function (action, urlParams={}) { // An entrypoint param is used for server-side metrics. If the current tab // is UITour, assume that it initiated the call to this method and override // the entrypoint accordingly. if (UITour.tourBrowsersByWindow.get(window) && UITour.tourBrowsersByWindow.get(window).has(gBrowser.selectedBrowser)) { urlParams.entrypoint = "uitour"; } let params = new URLSearchParams(); if (action) { params.set("action", action); } for (let name in urlParams) { if (urlParams[name] !== undefined) { params.set(name, urlParams[name]); } } let url = "about:accounts?" + params; switchToTabHavingURI(url, true, { replaceQueryString: true }); }, openSignInAgainPage: function (entryPoint) { this.openAccountsPage("reauth", { entrypoint: entryPoint }); }, }; XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () { return Cu.import("resource://gre/modules/FxAccountsCommon.js", {}); }); XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator", "resource://services-sync/FxaMigrator.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel", "resource://gre/modules/FxAccountsWebChannel.jsm");