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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 /* import-globals-from preferences.js */
7 const FXA_PAGE_LOGGED_OUT
= 0;
8 const FXA_PAGE_LOGGED_IN
= 1;
10 // Indexes into the "login status" deck.
11 // We are in a successful verified state - everything should work!
12 const FXA_LOGIN_VERIFIED
= 0;
13 // We have logged in to an unverified account.
14 const FXA_LOGIN_UNVERIFIED
= 1;
15 // We are logged in locally, but the server rejected our credentials.
16 const FXA_LOGIN_FAILED
= 2;
18 // Indexes into the "sync status" deck.
19 const SYNC_DISCONNECTED
= 0;
20 const SYNC_CONNECTED
= 1;
24 return document
.getElementById("weavePrefsDeck").selectedIndex
;
28 document
.getElementById("weavePrefsDeck").selectedIndex
= val
;
32 this._setupEventListeners();
33 this.setupEnginesUI();
37 .getElementById("weavePrefsDeck")
38 .removeAttribute("data-hidden-from-search");
40 // If the Service hasn't finished initializing, wait for it.
41 let xps
= Cc
["@mozilla.org/weave/service;1"].getService(
50 // it may take some time before all the promises we care about resolve, so
51 // pre-load what we can from synchronous sources.
52 this._showLoadPage(xps
);
54 let onUnload = function () {
55 window
.removeEventListener("unload", onUnload
);
57 Services
.obs
.removeObserver(onReady
, "weave:service:ready");
62 Services
.obs
.removeObserver(onReady
, "weave:service:ready");
63 window
.removeEventListener("unload", onUnload
);
67 Services
.obs
.addObserver(onReady
, "weave:service:ready");
68 window
.addEventListener("unload", onUnload
);
74 let maybeAcct
= false;
75 let username
= Services
.prefs
.getCharPref("services.sync.username", "");
77 document
.getElementById("fxaEmailAddress").textContent
= username
;
81 let cachedComputerName
= Services
.prefs
.getStringPref(
82 "identity.fxaccounts.account.device.name",
85 if (cachedComputerName
) {
87 this._populateComputerName(cachedComputerName
);
89 this.page
= maybeAcct
? FXA_PAGE_LOGGED_IN
: FXA_PAGE_LOGGED_OUT
;
93 Weave
.Svc
.Obs
.add(UIState
.ON_UPDATE
, this.updateWeavePrefs
, this);
95 window
.addEventListener("unload", () => {
96 Weave
.Svc
.Obs
.remove(UIState
.ON_UPDATE
, this.updateWeavePrefs
, this);
100 .promiseConnectDeviceURI(this._getEntryPoint())
101 .then(connectURI
=> {
103 .getElementById("connect-another-device")
104 .setAttribute("href", connectURI
);
106 // Links for mobile devices.
107 for (let platform
of ["android", "ios"]) {
109 Services
.prefs
.getCharPref(`identity.mobilepromo.${platform}`) +
111 for (let elt
of document
.querySelectorAll(
112 `.fxaMobilePromo-${platform}`
114 elt
.setAttribute("href", url
);
118 this.updateWeavePrefs();
120 // Notify observers that the UI is now ready
121 Services
.obs
.notifyObservers(window
, "sync-pane-loaded");
123 this._maybeShowSyncAction();
126 // Check if the user is coming from a call to action
127 // and show them the correct additional panel
128 _maybeShowSyncAction() {
130 location
.hash
== "#sync" &&
131 UIState
.get().status
== UIState
.STATUS_SIGNED_IN
133 if (location
.href
.includes("action=pair")) {
134 gSyncPane
.pairAnotherDevice();
135 } else if (location
.href
.includes("action=choose-what-to-sync")) {
136 gSyncPane
._chooseWhatToSync(false);
141 _toggleComputerNameControls(editMode
) {
142 let textbox
= document
.getElementById("fxaSyncComputerName");
143 textbox
.disabled
= !editMode
;
144 document
.getElementById("fxaChangeDeviceName").hidden
= editMode
;
145 document
.getElementById("fxaCancelChangeDeviceName").hidden
= !editMode
;
146 document
.getElementById("fxaSaveChangeDeviceName").hidden
= !editMode
;
149 _focusComputerNameTextbox() {
150 let textbox
= document
.getElementById("fxaSyncComputerName");
151 let valLength
= textbox
.value
.length
;
153 textbox
.setSelectionRange(valLength
, valLength
);
156 _blurComputerNameTextbox() {
157 document
.getElementById("fxaSyncComputerName").blur();
160 _focusAfterComputerNameTextbox() {
161 // Focus the most appropriate element that's *not* the "computer name" box.
162 Services
.focus
.moveFocus(
164 document
.getElementById("fxaSyncComputerName"),
165 Services
.focus
.MOVEFOCUS_FORWARD
,
170 _updateComputerNameValue(save
) {
172 let textbox
= document
.getElementById("fxaSyncComputerName");
173 Weave
.Service
.clientsEngine
.localName
= textbox
.value
;
175 this._populateComputerName(Weave
.Service
.clientsEngine
.localName
);
178 _setupEventListeners() {
179 function setEventListener(aId
, aEventType
, aCallback
) {
182 .addEventListener(aEventType
, aCallback
.bind(gSyncPane
));
185 setEventListener("openChangeProfileImage", "click", function (event
) {
186 gSyncPane
.openChangeProfileImage(event
);
188 setEventListener("openChangeProfileImage", "keypress", function (event
) {
189 gSyncPane
.openChangeProfileImage(event
);
191 setEventListener("fxaChangeDeviceName", "command", function () {
192 this._toggleComputerNameControls(true);
193 this._focusComputerNameTextbox();
195 setEventListener("fxaCancelChangeDeviceName", "command", function () {
196 // We explicitly blur the textbox because of bug 75324, then after
197 // changing the state of the buttons, force focus to whatever the focus
198 // manager thinks should be next (which on the mac, depends on an OSX
199 // keyboard access preference)
200 this._blurComputerNameTextbox();
201 this._toggleComputerNameControls(false);
202 this._updateComputerNameValue(false);
203 this._focusAfterComputerNameTextbox();
205 setEventListener("fxaSaveChangeDeviceName", "command", function () {
206 // Work around bug 75324 - see above.
207 this._blurComputerNameTextbox();
208 this._toggleComputerNameControls(false);
209 this._updateComputerNameValue(true);
210 this._focusAfterComputerNameTextbox();
212 setEventListener("noFxaSignIn", "command", function () {
216 setEventListener("fxaUnlinkButton", "command", function () {
217 gSyncPane
.unlinkFirefoxAccount(true);
222 gSyncPane
.verifyFirefoxAccount
224 setEventListener("unverifiedUnlinkFxaAccount", "command", function () {
225 /* no warning as account can't have previously synced */
226 gSyncPane
.unlinkFirefoxAccount(false);
228 setEventListener("rejectReSignIn", "command", function () {
229 gSyncPane
.reSignIn(this._getEntryPoint());
231 setEventListener("rejectUnlinkFxaAccount", "command", function () {
232 gSyncPane
.unlinkFirefoxAccount(true);
234 setEventListener("fxaSyncComputerName", "keypress", function (e
) {
235 if (e
.keyCode
== KeyEvent
.DOM_VK_RETURN
) {
236 document
.getElementById("fxaSaveChangeDeviceName").click();
237 } else if (e
.keyCode
== KeyEvent
.DOM_VK_ESCAPE
) {
238 document
.getElementById("fxaCancelChangeDeviceName").click();
241 setEventListener("syncSetup", "command", function () {
242 this._chooseWhatToSync(false);
244 setEventListener("syncChangeOptions", "command", function () {
245 this._chooseWhatToSync(true);
247 setEventListener("syncNow", "command", function () {
248 // syncing can take a little time to send the "started" notification, so
249 // pretend we already got it.
250 this._updateSyncNow(true);
251 Weave
.Service
.sync({ why
: "aboutprefs" });
253 setEventListener("syncNow", "mouseover", function () {
254 const state
= UIState
.get();
255 // If we are currently syncing, just set the tooltip to the same as the
256 // button label (ie, "Syncing...")
257 let tooltiptext
= state
.syncing
258 ? document
.getElementById("syncNow").getAttribute("label")
259 : window
.browsingContext
.topChromeWindow
.gSync
.formatLastSyncDate(
263 .getElementById("syncNow")
264 .setAttribute("tooltiptext", tooltiptext
);
269 const state
= UIState
.get();
270 const isSyncEnabled
= state
.syncEnabled
;
271 let syncStatusTitle
= document
.getElementById("syncStatusTitle");
272 let syncNowButton
= document
.getElementById("syncNow");
273 let syncNotConfiguredEl
= document
.getElementById("syncNotConfigured");
274 let syncConfiguredEl
= document
.getElementById("syncConfigured");
277 syncStatusTitle
.setAttribute("data-l10n-id", "prefs-syncing-on");
278 syncNowButton
.hidden
= false;
279 syncConfiguredEl
.hidden
= false;
280 syncNotConfiguredEl
.hidden
= true;
282 syncStatusTitle
.setAttribute("data-l10n-id", "prefs-syncing-off");
283 syncNowButton
.hidden
= true;
284 syncConfiguredEl
.hidden
= true;
285 syncNotConfiguredEl
.hidden
= false;
289 async
_chooseWhatToSync(isAlreadySyncing
) {
290 // Assuming another device is syncing and we're not,
291 // we update the engines selection so the correct
292 // checkboxes are pre-filed.
293 if (!isAlreadySyncing
) {
295 await Weave
.Service
.updateLocalEnginesState();
297 console
.error("Error updating the local engines state", err
);
301 if (isAlreadySyncing
) {
302 // If we are already syncing then we also offer to disconnect.
303 params
.disconnectFun
= () => this.disconnectSync();
306 "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml",
308 closingCallback
: event
=> {
309 if (!isAlreadySyncing
&& event
.detail
.button
== "accept") {
310 // We weren't syncing but the user has accepted the dialog - so we
313 .recordConnection(["sync"], "ui")
316 return Weave
.Service
.configure();
319 console
.error("Failed to enable sync", err
);
322 // When the modal closes we want to remove any query params
323 // so it doesn't open on subsequent visits (and will reload)
324 const browser
= window
.docShell
.chromeEventHandler
;
325 browser
.loadURI(Services
.io
.newURI("about:preferences#sync"), {
327 Services
.scriptSecurityManager
.getSystemPrincipal(),
335 _updateSyncNow(syncing
) {
336 let butSyncNow
= document
.getElementById("syncNow");
337 let fluentID
= syncing
? "prefs-syncing-button" : "prefs-sync-now-button";
338 if (document
.l10n
.getAttributes(butSyncNow
).id
!= fluentID
) {
339 // Only one of the two strings has an accesskey, and fluent won't
340 // remove it if we switch to the string that doesn't, so just force
342 butSyncNow
.removeAttribute("accesskey");
343 document
.l10n
.setAttributes(butSyncNow
, fluentID
);
345 butSyncNow
.disabled
= syncing
;
349 let service
= Cc
["@mozilla.org/weave/service;1"].getService(
353 let displayNameLabel
= document
.getElementById("fxaDisplayName");
354 let fxaEmailAddressLabels
= document
.querySelectorAll(
355 ".l10nArgsEmailAddress"
357 displayNameLabel
.hidden
= true;
359 // while we determine the fxa status pre-load what we can.
360 this._showLoadPage(service
);
362 let state
= UIState
.get();
363 if (state
.status
== UIState
.STATUS_NOT_CONFIGURED
) {
364 this.page
= FXA_PAGE_LOGGED_OUT
;
367 this.page
= FXA_PAGE_LOGGED_IN
;
368 // We are logged in locally, but maybe we are in a state where the
369 // server rejected our credentials (eg, password changed on the server)
370 let fxaLoginStatus
= document
.getElementById("fxaLoginStatus");
371 let syncReady
= false; // Is sync able to actually sync?
372 // We need to check error states that need a re-authenticate to resolve
374 if (state
.status
== UIState
.STATUS_LOGIN_FAILED
) {
375 fxaLoginStatus
.selectedIndex
= FXA_LOGIN_FAILED
;
376 } else if (state
.status
== UIState
.STATUS_NOT_VERIFIED
) {
377 fxaLoginStatus
.selectedIndex
= FXA_LOGIN_UNVERIFIED
;
379 // We must be golden (or in an error state we expect to magically
381 fxaLoginStatus
.selectedIndex
= FXA_LOGIN_VERIFIED
;
384 fxaEmailAddressLabels
.forEach(label
=> {
385 let l10nAttrs
= document
.l10n
.getAttributes(label
);
386 document
.l10n
.setAttributes(label
, l10nAttrs
.id
, { email
: state
.email
});
388 document
.getElementById("fxaEmailAddress").textContent
= state
.email
;
390 this._populateComputerName(Weave
.Service
.clientsEngine
.localName
);
391 for (let elt
of document
.querySelectorAll(".needs-account-ready")) {
392 elt
.disabled
= !syncReady
;
395 // Clear the profile image (if any) of the previously logged in account.
397 .querySelector("#fxaLoginVerified > .fxaProfileImage")
398 .style
.removeProperty("list-style-image");
400 if (state
.displayName
) {
401 fxaLoginStatus
.setAttribute("hasName", true);
402 displayNameLabel
.hidden
= false;
403 document
.getElementById("fxaDisplayNameHeading").textContent
=
406 fxaLoginStatus
.removeAttribute("hasName");
408 if (state
.avatarURL
&& !state
.avatarIsDefault
) {
409 let bgImage
= 'url("' + state
.avatarURL
+ '")';
410 let profileImageElement
= document
.querySelector(
411 "#fxaLoginVerified > .fxaProfileImage"
413 profileImageElement
.style
.listStyleImage
= bgImage
;
415 let img
= new Image();
416 img
.onerror
= () => {
417 // Clear the image if it has trouble loading. Since this callback is asynchronous
418 // we check to make sure the image is still the same before we clear it.
419 if (profileImageElement
.style
.listStyleImage
=== bgImage
) {
420 profileImageElement
.style
.removeProperty("list-style-image");
423 img
.src
= state
.avatarURL
;
425 // The "manage account" link embeds the uid, so we need to update this
426 // if the account state changes.
428 .promiseManageURI(this._getEntryPoint())
429 .then(accountsManageURI
=> {
431 .getElementById("verifiedManage")
432 .setAttribute("href", accountsManageURI
);
434 // and the actual sync state.
435 let eltSyncStatus
= document
.getElementById("syncStatusContainer");
436 eltSyncStatus
.hidden
= !syncReady
;
437 this._updateSyncNow(state
.syncing
);
442 let params
= new URLSearchParams(
443 document
.URL
.split("#")[0].split("?")[1] || ""
445 return params
.get("entrypoint") || "preferences";
448 openContentInBrowser(url
, options
) {
449 let win
= Services
.wm
.getMostRecentWindow("navigator:browser");
451 openTrustedLinkIn(url
, "tab");
454 win
.switchToTabHavingURI(url
, true, options
);
457 // Replace the current tab with the specified URL.
458 replaceTabWithUrl(url
) {
459 // Get the <browser> element hosting us.
460 let browser
= window
.docShell
.chromeEventHandler
;
461 // And tell it to load our URL.
462 browser
.loadURI(Services
.io
.newURI(url
), {
463 triggeringPrincipal
: Services
.scriptSecurityManager
.createNullPrincipal(
470 if (!(await FxAccounts
.canConnectAccount())) {
473 const url
= await FxAccounts
.config
.promiseConnectAccountURI(
474 this._getEntryPoint()
476 this.replaceTabWithUrl(url
);
480 * Attempts to take the user through the sign in flow by opening the web content
481 * with the given entrypoint as a query parameter
482 * @param entrypoint: An string appended to the query parameters, used in telemtry to differentiate
483 * different entrypoints to accounts
485 async
reSignIn(entrypoint
) {
486 const url
= await FxAccounts
.config
.promiseConnectAccountURI(entrypoint
);
487 this.replaceTabWithUrl(url
);
490 clickOrSpaceOrEnterPressed(event
) {
491 // Note: charCode is deprecated, but 'char' not yet implemented.
492 // Replace charCode with char when implemented, see Bug 680830
494 (event
.type
== "click" && event
.button
== 0) ||
495 (event
.type
== "keypress" &&
496 (event
.charCode
== KeyEvent
.DOM_VK_SPACE
||
497 event
.keyCode
== KeyEvent
.DOM_VK_RETURN
))
501 openChangeProfileImage(event
) {
502 if (this.clickOrSpaceOrEnterPressed(event
)) {
504 .promiseChangeAvatarURI(this._getEntryPoint())
506 this.openContentInBrowser(url
, {
507 replaceQueryString
: true,
509 Services
.scriptSecurityManager
.getSystemPrincipal(),
512 // Prevent page from scrolling on the space key.
513 event
.preventDefault();
517 async
verifyFirefoxAccount() {
518 return this.reSignIn("preferences-reverify");
521 // Disconnect the account, including everything linked.
522 unlinkFirefoxAccount(confirm
) {
523 window
.browsingContext
.topChromeWindow
.gSync
.disconnect({
528 // Disconnect sync, leaving the account connected.
530 return window
.browsingContext
.topChromeWindow
.gSync
.disconnect({
532 disconnectAccount
: false,
536 pairAnotherDevice() {
538 "chrome://browser/content/preferences/fxaPairDevice.xhtml",
539 { features
: "resizable=no" }
543 _populateComputerName(value
) {
544 let textbox
= document
.getElementById("fxaSyncComputerName");
545 if (!textbox
.hasAttribute("placeholder")) {
546 textbox
.setAttribute(
548 fxAccounts
.device
.getDefaultLocalName()
551 textbox
.value
= value
;
554 // arranges to dynamically show or hide sync engine name elements based on the
555 // preferences used for this engines.
557 let observe
= (elt
, prefName
) => {
558 elt
.hidden
= !Services
.prefs
.getBoolPref(prefName
, false);
561 for (let elt
of document
.querySelectorAll("[engine_preference]")) {
562 let prefName
= elt
.getAttribute("engine_preference");
563 let obs
= observe
.bind(null, elt
, prefName
);
565 Services
.prefs
.addObserver(prefName
, obs
);
566 window
.addEventListener("unload", () => {
567 Services
.prefs
.removeObserver(prefName
, obs
);