# 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/. let gFxAccounts = { PREF_SYNC_START_DOORHANGER: "services.sync.ui.showSyncStartDoorhanger", DOORHANGER_ACTIVATE_DELAY_MS: 5000, SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration", _initialized: false, _inCustomizationMode: false, // _expectingNotifyClose is a hack that helps us determine if the // migration notification was closed due to being "dismissed" vs closed // due to one of the migration buttons being clicked. It's ugly and somewhat // fragile, so bug 1119020 exists to help us do this better. _expectingNotifyClose: 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.ONVERIFIED_NOTIFICATION, this.FxAccountsCommon.ONLOGOUT_NOTIFICATION, "weave:notification:removed", 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); // Request the current Legacy-Sync-to-FxA migration status. We'll be // notified of fxa-migration:state-changed in response if necessary. Services.obs.notifyObservers(null, "fxa-migration:state-request", null); let contentUri = Services.urlFormatter.formatURLPref("identity.fxaccounts.remote.webchannel.uri"); // The FxAccountsWebChannel listens for events and updates // the state machine accordingly. let fxAccountsWebChannel = new FxAccountsWebChannel({ content_uri: contentUri, channel_id: this.FxAccountsCommon.WEBCHANNEL_ID }); 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 "weave:notification:removed": // this exists just so we can tell the difference between "box was // closed due to button press" vs "was closed due to click on [x]" let notif = subject.wrappedJSObject.object; if (notif.title == this.SYNC_MIGRATION_NOTIFICATION_TITLE && !this._expectingNotifyClose) { // it's an [x] on our notification, so record telemetry. this.fxaMigrator.recordTelemetry(this.fxaMigrator.TELEMETRY_DECLINED); } 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 (newState, email) { this._migrationInfo = !newState ? null : { state: newState, email: email ? email.QueryInterface(Ci.nsISupportsString).data : null, }; this.updateUI(); }, 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"); }, showSyncFailedDoorhanger: function () { this.showDoorhanger("sync-error-panel"); }, updateUI: function () { this.updateAppMenuItem(); this.updateMigrationNotification(); }, updateAppMenuItem: function () { if (this._migrationInfo) { this.updateAppMenuItemForMigration(); return; } 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) { // When migration transitions from needs-verification to the null state, // fxAccountsEnabled is false because migration has not yet finished. In // that case, hide the button. We'll get another notification with a null // state once migration is complete. this.panelUIFooter.removeAttribute("fxastatus"); return; } // 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 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"); 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); } else { this.panelUIFooter.setAttribute("fxastatus", "signedin"); this.panelUILabel.setAttribute("label", userData.email); this.panelUIStatus.setAttribute("tooltiptext", signedInTooltiptext); } if (profileInfoEnabled) { this.panelUIFooter.setAttribute("fxaprofileimage", "enabled"); } } } let updateWithProfile = (profile) => { if (!this._inCustomizationMode && profileInfoEnabled) { if (profile.displayName) { this.panelUILabel.setAttribute("label", profile.displayName); } if (profile.avatar) { let img = new Image(); // Make sure the image is available before attempting to display it img.onload = () => { this.panelUIFooter.setAttribute("fxaprofileimage", "set"); this.panelUIAvatar.style.listStyleImage = "url('" + profile.avatar + "')"; }; img.src = profile.avatar; } } } // Calling getSignedInUserProfile() without a user logged in causes log // noise that looks like an actual error... fxAccounts.getSignedInUser().then(userData => { // userData may be null here when the user is not signed-in, but that's expected updateWithUserData(userData); return userData ? fxAccounts.getSignedInUserProfile() : 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 profile", error); updateWithUserData(null); }); }, updateAppMenuItemForMigration: Task.async(function* () { let status = null; let label = null; switch (this._migrationInfo.state) { case this.fxaMigrator.STATE_USER_FXA: status = "migrate-signup"; label = this.strings.formatStringFromName("needUserShort", [this.panelUILabel.getAttribute("fxabrandname")], 1); break; case this.fxaMigrator.STATE_USER_FXA_VERIFIED: status = "migrate-verify"; label = this.strings.formatStringFromName("needVerifiedUserShort", [this._migrationInfo.email], 1); break; } this.panelUILabel.label = label; this.panelUIFooter.setAttribute("fxastatus", status); }), updateMigrationNotification: Task.async(function* () { if (!this._migrationInfo) { this._expectingNotifyClose = true; Weave.Notifications.removeAll(this.SYNC_MIGRATION_NOTIFICATION_TITLE); // because this is called even when there is no such notification, we // set _expectingNotifyClose back to false as we may yet create a new // notification (but in general, once we've created a migration // notification once in a session, we don't create one again) this._expectingNotifyClose = false; return; } let note = null; switch (this._migrationInfo.state) { case this.fxaMigrator.STATE_USER_FXA: { // There are 2 cases here - no email address means it is an offer on // the first device (so the user is prompted to create an account). // If there is an email address it is the "join the party" flow, so the // user is prompted to sign in with the address they previously used. let msg, upgradeLabel, upgradeAccessKey, learnMoreLink; if (this._migrationInfo.email) { msg = this.strings.formatStringFromName("signInAfterUpgradeOnOtherDevice.description", [this._migrationInfo.email], 1); upgradeLabel = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.label"); upgradeAccessKey = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.accessKey"); } else { msg = this.strings.GetStringFromName("needUserLong"); upgradeLabel = this.strings.GetStringFromName("upgradeToFxA.label"); upgradeAccessKey = this.strings.GetStringFromName("upgradeToFxA.accessKey"); learnMoreLink = this.fxaMigrator.learnMoreLink; } note = new Weave.Notification( undefined, msg, undefined, Weave.Notifications.PRIORITY_WARNING, [ new Weave.NotificationButton(upgradeLabel, upgradeAccessKey, () => { this._expectingNotifyClose = true; this.fxaMigrator.createFxAccount(window); }), ], learnMoreLink ); break; } case this.fxaMigrator.STATE_USER_FXA_VERIFIED: { let msg = this.strings.formatStringFromName("needVerifiedUserLong", [this._migrationInfo.email], 1); let resendLabel = this.strings.GetStringFromName("resendVerificationEmail.label"); let resendAccessKey = this.strings.GetStringFromName("resendVerificationEmail.accessKey"); note = new Weave.Notification( undefined, msg, undefined, Weave.Notifications.PRIORITY_INFO, [ new Weave.NotificationButton(resendLabel, resendAccessKey, () => { this._expectingNotifyClose = true; this.fxaMigrator.resendVerificationMail(); }), ] ); break; } } note.title = this.SYNC_MIGRATION_NOTIFICATION_TITLE; Weave.Notifications.replaceTitle(note); }), onMenuPanelCommand: function () { switch (this.panelUIFooter.getAttribute("fxastatus")) { case "signedin": this.openPreferences(); break; case "error": this.openSignInAgainPage("menupanel"); break; case "migrate-signup": case "migrate-verify": // The migration flow calls for the menu item to open sync prefs rather // than requesting migration start immediately. this.openPreferences(); break; default: this.openAccountsPage(null, { entryPoint: "menupanel" }); break; } PanelUI.hide(); }, openPreferences: function () { openPreferences("paneSync"); }, 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, "FxAccountsWebChannel", "resource://gre/modules/FxAccountsWebChannel.jsm");