gecko-dev/browser/base/content/browser-fxaccounts.js

487 lines
17 KiB
JavaScript

# 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");