Bug 1824490 - Use the end page value rather than the start page value of the previous...
[gecko.git] / browser / components / preferences / sync.js
blobe02c9bf18aaf629c09b7058877eb2e9f2b827cba
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 XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function() {
8   return ChromeUtils.import("resource://gre/modules/FxAccountsCommon.js");
9 });
11 const FXA_PAGE_LOGGED_OUT = 0;
12 const FXA_PAGE_LOGGED_IN = 1;
14 // Indexes into the "login status" deck.
15 // We are in a successful verified state - everything should work!
16 const FXA_LOGIN_VERIFIED = 0;
17 // We have logged in to an unverified account.
18 const FXA_LOGIN_UNVERIFIED = 1;
19 // We are logged in locally, but the server rejected our credentials.
20 const FXA_LOGIN_FAILED = 2;
22 // Indexes into the "sync status" deck.
23 const SYNC_DISCONNECTED = 0;
24 const SYNC_CONNECTED = 1;
26 var gSyncPane = {
27   get page() {
28     return document.getElementById("weavePrefsDeck").selectedIndex;
29   },
31   set page(val) {
32     document.getElementById("weavePrefsDeck").selectedIndex = val;
33   },
35   init() {
36     this._setupEventListeners();
37     this.setupEnginesUI();
39     document
40       .getElementById("weavePrefsDeck")
41       .removeAttribute("data-hidden-from-search");
43     // If the Service hasn't finished initializing, wait for it.
44     let xps = Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
45       .wrappedJSObject;
47     if (xps.ready) {
48       this._init();
49       return;
50     }
52     // it may take some time before all the promises we care about resolve, so
53     // pre-load what we can from synchronous sources.
54     this._showLoadPage(xps);
56     let onUnload = function() {
57       window.removeEventListener("unload", onUnload);
58       try {
59         Services.obs.removeObserver(onReady, "weave:service:ready");
60       } catch (e) {}
61     };
63     let onReady = () => {
64       Services.obs.removeObserver(onReady, "weave:service:ready");
65       window.removeEventListener("unload", onUnload);
66       this._init();
67     };
69     Services.obs.addObserver(onReady, "weave:service:ready");
70     window.addEventListener("unload", onUnload);
72     xps.ensureLoaded();
73   },
75   _showLoadPage(xps) {
76     let maybeAcct = false;
77     let username = Services.prefs.getCharPref("services.sync.username", "");
78     if (username) {
79       document.getElementById("fxaEmailAddress").textContent = username;
80       maybeAcct = true;
81     }
83     let cachedComputerName = Services.prefs.getStringPref(
84       "identity.fxaccounts.account.device.name",
85       ""
86     );
87     if (cachedComputerName) {
88       maybeAcct = true;
89       this._populateComputerName(cachedComputerName);
90     }
91     this.page = maybeAcct ? FXA_PAGE_LOGGED_IN : FXA_PAGE_LOGGED_OUT;
92   },
94   _init() {
95     Weave.Svc.Obs.add(UIState.ON_UPDATE, this.updateWeavePrefs, this);
97     window.addEventListener("unload", () => {
98       Weave.Svc.Obs.remove(UIState.ON_UPDATE, this.updateWeavePrefs, this);
99     });
101     XPCOMUtils.defineLazyGetter(this, "_accountsStringBundle", () => {
102       return Services.strings.createBundle(
103         "chrome://browser/locale/accounts.properties"
104       );
105     });
107     FxAccounts.config
108       .promiseConnectDeviceURI(this._getEntryPoint())
109       .then(connectURI => {
110         document
111           .getElementById("connect-another-device")
112           .setAttribute("href", connectURI);
113       });
114     // Links for mobile devices.
115     for (let platform of ["android", "ios"]) {
116       let url =
117         Services.prefs.getCharPref(`identity.mobilepromo.${platform}`) +
118         "sync-preferences";
119       for (let elt of document.querySelectorAll(
120         `.fxaMobilePromo-${platform}`
121       )) {
122         elt.setAttribute("href", url);
123       }
124     }
126     this.updateWeavePrefs();
128     // Notify observers that the UI is now ready
129     Services.obs.notifyObservers(window, "sync-pane-loaded");
131     if (
132       location.hash == "#sync" &&
133       UIState.get().status == UIState.STATUS_SIGNED_IN
134     ) {
135       if (location.href.includes("action=pair")) {
136         gSyncPane.pairAnotherDevice();
137       } else if (location.href.includes("action=choose-what-to-sync")) {
138         gSyncPane._chooseWhatToSync(false);
139       }
140     }
141   },
143   _toggleComputerNameControls(editMode) {
144     let textbox = document.getElementById("fxaSyncComputerName");
145     textbox.disabled = !editMode;
146     document.getElementById("fxaChangeDeviceName").hidden = editMode;
147     document.getElementById("fxaCancelChangeDeviceName").hidden = !editMode;
148     document.getElementById("fxaSaveChangeDeviceName").hidden = !editMode;
149   },
151   _focusComputerNameTextbox() {
152     let textbox = document.getElementById("fxaSyncComputerName");
153     let valLength = textbox.value.length;
154     textbox.focus();
155     textbox.setSelectionRange(valLength, valLength);
156   },
158   _blurComputerNameTextbox() {
159     document.getElementById("fxaSyncComputerName").blur();
160   },
162   _focusAfterComputerNameTextbox() {
163     // Focus the most appropriate element that's *not* the "computer name" box.
164     Services.focus.moveFocus(
165       window,
166       document.getElementById("fxaSyncComputerName"),
167       Services.focus.MOVEFOCUS_FORWARD,
168       0
169     );
170   },
172   _updateComputerNameValue(save) {
173     if (save) {
174       let textbox = document.getElementById("fxaSyncComputerName");
175       Weave.Service.clientsEngine.localName = textbox.value;
176     }
177     this._populateComputerName(Weave.Service.clientsEngine.localName);
178   },
180   _setupEventListeners() {
181     function setEventListener(aId, aEventType, aCallback) {
182       document
183         .getElementById(aId)
184         .addEventListener(aEventType, aCallback.bind(gSyncPane));
185     }
187     setEventListener("openChangeProfileImage", "click", function(event) {
188       gSyncPane.openChangeProfileImage(event);
189     });
190     setEventListener("openChangeProfileImage", "keypress", function(event) {
191       gSyncPane.openChangeProfileImage(event);
192     });
193     setEventListener("fxaChangeDeviceName", "command", function() {
194       this._toggleComputerNameControls(true);
195       this._focusComputerNameTextbox();
196     });
197     setEventListener("fxaCancelChangeDeviceName", "command", function() {
198       // We explicitly blur the textbox because of bug 75324, then after
199       // changing the state of the buttons, force focus to whatever the focus
200       // manager thinks should be next (which on the mac, depends on an OSX
201       // keyboard access preference)
202       this._blurComputerNameTextbox();
203       this._toggleComputerNameControls(false);
204       this._updateComputerNameValue(false);
205       this._focusAfterComputerNameTextbox();
206     });
207     setEventListener("fxaSaveChangeDeviceName", "command", function() {
208       // Work around bug 75324 - see above.
209       this._blurComputerNameTextbox();
210       this._toggleComputerNameControls(false);
211       this._updateComputerNameValue(true);
212       this._focusAfterComputerNameTextbox();
213     });
214     setEventListener("noFxaSignIn", "command", function() {
215       gSyncPane.signIn();
216       return false;
217     });
218     setEventListener("fxaUnlinkButton", "command", function() {
219       gSyncPane.unlinkFirefoxAccount(true);
220     });
221     setEventListener(
222       "verifyFxaAccount",
223       "command",
224       gSyncPane.verifyFirefoxAccount
225     );
226     setEventListener("unverifiedUnlinkFxaAccount", "command", function() {
227       /* no warning as account can't have previously synced */
228       gSyncPane.unlinkFirefoxAccount(false);
229     });
230     setEventListener("rejectReSignIn", "command", gSyncPane.reSignIn);
231     setEventListener("rejectUnlinkFxaAccount", "command", function() {
232       gSyncPane.unlinkFirefoxAccount(true);
233     });
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();
239       }
240     });
241     setEventListener("syncSetup", "command", function() {
242       this._chooseWhatToSync(false);
243     });
244     setEventListener("syncChangeOptions", "command", function() {
245       this._chooseWhatToSync(true);
246     });
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" });
252     });
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(
260             state.lastSync
261           );
262       document
263         .getElementById("syncNow")
264         .setAttribute("tooltiptext", tooltiptext);
265     });
266   },
268   async _chooseWhatToSync(isAlreadySyncing) {
269     // Assuming another device is syncing and we're not,
270     // we update the engines selection so the correct
271     // checkboxes are pre-filed.
272     if (!isAlreadySyncing) {
273       try {
274         await Weave.Service.updateLocalEnginesState();
275       } catch (err) {
276         console.error("Error updating the local engines state", err);
277       }
278     }
279     let params = {};
280     if (isAlreadySyncing) {
281       // If we are already syncing then we also offer to disconnect.
282       params.disconnectFun = () => this.disconnectSync();
283     }
284     gSubDialog.open(
285       "chrome://browser/content/preferences/dialogs/syncChooseWhatToSync.xhtml",
286       {
287         closingCallback: event => {
288           if (!isAlreadySyncing && event.detail.button == "accept") {
289             // We weren't syncing but the user has accepted the dialog - so we
290             // want to start!
291             fxAccounts.telemetry
292               .recordConnection(["sync"], "ui")
293               .then(() => {
294                 return Weave.Service.configure();
295               })
296               .catch(err => {
297                 console.error("Failed to enable sync", err);
298               });
299           }
300         },
301       },
302       params /* aParams */
303     );
304   },
306   _updateSyncNow(syncing) {
307     let butSyncNow = document.getElementById("syncNow");
308     if (syncing) {
309       butSyncNow.setAttribute("label", butSyncNow.getAttribute("labelsyncing"));
310       butSyncNow.removeAttribute("accesskey");
311       butSyncNow.disabled = true;
312     } else {
313       butSyncNow.setAttribute(
314         "label",
315         butSyncNow.getAttribute("labelnotsyncing")
316       );
317       butSyncNow.setAttribute(
318         "accesskey",
319         butSyncNow.getAttribute("accesskeynotsyncing")
320       );
321       butSyncNow.disabled = false;
322     }
323   },
325   updateWeavePrefs() {
326     let service = Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports)
327       .wrappedJSObject;
329     let displayNameLabel = document.getElementById("fxaDisplayName");
330     let fxaEmailAddressLabels = document.querySelectorAll(
331       ".l10nArgsEmailAddress"
332     );
333     displayNameLabel.hidden = true;
335     // while we determine the fxa status pre-load what we can.
336     this._showLoadPage(service);
338     let state = UIState.get();
339     if (state.status == UIState.STATUS_NOT_CONFIGURED) {
340       this.page = FXA_PAGE_LOGGED_OUT;
341       return;
342     }
343     this.page = FXA_PAGE_LOGGED_IN;
344     // We are logged in locally, but maybe we are in a state where the
345     // server rejected our credentials (eg, password changed on the server)
346     let fxaLoginStatus = document.getElementById("fxaLoginStatus");
347     let syncReady = false; // Is sync able to actually sync?
348     // We need to check error states that need a re-authenticate to resolve
349     // themselves first.
350     if (state.status == UIState.STATUS_LOGIN_FAILED) {
351       fxaLoginStatus.selectedIndex = FXA_LOGIN_FAILED;
352     } else if (state.status == UIState.STATUS_NOT_VERIFIED) {
353       fxaLoginStatus.selectedIndex = FXA_LOGIN_UNVERIFIED;
354     } else {
355       // We must be golden (or in an error state we expect to magically
356       // resolve itself)
357       fxaLoginStatus.selectedIndex = FXA_LOGIN_VERIFIED;
358       syncReady = true;
359     }
360     fxaEmailAddressLabels.forEach(label => {
361       let l10nAttrs = document.l10n.getAttributes(label);
362       document.l10n.setAttributes(label, l10nAttrs.id, { email: state.email });
363     });
364     document.getElementById("fxaEmailAddress").textContent = state.email;
366     this._populateComputerName(Weave.Service.clientsEngine.localName);
367     for (let elt of document.querySelectorAll(".needs-account-ready")) {
368       elt.disabled = !syncReady;
369     }
371     // Clear the profile image (if any) of the previously logged in account.
372     document
373       .querySelector("#fxaLoginVerified > .fxaProfileImage")
374       .style.removeProperty("list-style-image");
376     if (state.displayName) {
377       fxaLoginStatus.setAttribute("hasName", true);
378       displayNameLabel.hidden = false;
379       document.getElementById("fxaDisplayNameHeading").textContent =
380         state.displayName;
381     } else {
382       fxaLoginStatus.removeAttribute("hasName");
383     }
384     if (state.avatarURL && !state.avatarIsDefault) {
385       let bgImage = 'url("' + state.avatarURL + '")';
386       let profileImageElement = document.querySelector(
387         "#fxaLoginVerified > .fxaProfileImage"
388       );
389       profileImageElement.style.listStyleImage = bgImage;
391       let img = new Image();
392       img.onerror = () => {
393         // Clear the image if it has trouble loading. Since this callback is asynchronous
394         // we check to make sure the image is still the same before we clear it.
395         if (profileImageElement.style.listStyleImage === bgImage) {
396           profileImageElement.style.removeProperty("list-style-image");
397         }
398       };
399       img.src = state.avatarURL;
400     }
401     // The "manage account" link embeds the uid, so we need to update this
402     // if the account state changes.
403     FxAccounts.config
404       .promiseManageURI(this._getEntryPoint())
405       .then(accountsManageURI => {
406         document
407           .getElementById("verifiedManage")
408           .setAttribute("href", accountsManageURI);
409       });
410     // and the actual sync state.
411     let eltSyncStatus = document.getElementById("syncStatus");
412     eltSyncStatus.hidden = !syncReady;
413     eltSyncStatus.selectedIndex = state.syncEnabled
414       ? SYNC_CONNECTED
415       : SYNC_DISCONNECTED;
416     this._updateSyncNow(state.syncing);
417   },
419   _getEntryPoint() {
420     let params = new URLSearchParams(
421       document.URL.split("#")[0].split("?")[1] || ""
422     );
423     return params.get("entrypoint") || "preferences";
424   },
426   openContentInBrowser(url, options) {
427     let win = Services.wm.getMostRecentWindow("navigator:browser");
428     if (!win) {
429       openTrustedLinkIn(url, "tab");
430       return;
431     }
432     win.switchToTabHavingURI(url, true, options);
433   },
435   // Replace the current tab with the specified URL.
436   replaceTabWithUrl(url) {
437     // Get the <browser> element hosting us.
438     let browser = window.docShell.chromeEventHandler;
439     // And tell it to load our URL.
440     browser.loadURI(Services.io.newURI(url), {
441       triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
442         {}
443       ),
444     });
445   },
447   async signIn() {
448     if (!(await FxAccounts.canConnectAccount())) {
449       return;
450     }
451     const url = await FxAccounts.config.promiseConnectAccountURI(
452       this._getEntryPoint()
453     );
454     this.replaceTabWithUrl(url);
455   },
457   async reSignIn() {
458     // There's a bit of an edge-case here - we might be forcing reauth when we've
459     // lost the FxA account data - in which case we'll not get a URL as the re-auth
460     // URL embeds account info and the server endpoint complains if we don't
461     // supply it - So we just use the regular "sign in" URL in that case.
462     if (!(await FxAccounts.canConnectAccount())) {
463       return;
464     }
466     let entryPoint = this._getEntryPoint();
467     const url =
468       (await FxAccounts.config.promiseForceSigninURI(entryPoint)) ||
469       (await FxAccounts.config.promiseConnectAccountURI(entryPoint));
470     this.replaceTabWithUrl(url);
471   },
473   clickOrSpaceOrEnterPressed(event) {
474     // Note: charCode is deprecated, but 'char' not yet implemented.
475     // Replace charCode with char when implemented, see Bug 680830
476     return (
477       (event.type == "click" && event.button == 0) ||
478       (event.type == "keypress" &&
479         (event.charCode == KeyEvent.DOM_VK_SPACE ||
480           event.keyCode == KeyEvent.DOM_VK_RETURN))
481     );
482   },
484   openChangeProfileImage(event) {
485     if (this.clickOrSpaceOrEnterPressed(event)) {
486       FxAccounts.config
487         .promiseChangeAvatarURI(this._getEntryPoint())
488         .then(url => {
489           this.openContentInBrowser(url, {
490             replaceQueryString: true,
491             triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
492           });
493         });
494       // Prevent page from scrolling on the space key.
495       event.preventDefault();
496     }
497   },
499   verifyFirefoxAccount() {
500     let showVerifyNotification = data => {
501       let isError = !data;
502       let maybeNot = isError ? "Not" : "";
503       let sb = this._accountsStringBundle;
504       let title = sb.GetStringFromName("verification" + maybeNot + "SentTitle");
505       let email = !isError && data ? data.email : "";
506       let body = sb.formatStringFromName(
507         "verification" + maybeNot + "SentBody",
508         [email]
509       );
510       new Notification(title, { body });
511     };
513     let onError = () => {
514       showVerifyNotification();
515     };
517     let onSuccess = data => {
518       if (data) {
519         showVerifyNotification(data);
520       } else {
521         onError();
522       }
523     };
525     fxAccounts
526       .resendVerificationEmail()
527       .then(() => fxAccounts.getSignedInUser(), onError)
528       .then(onSuccess, onError);
529   },
531   // Disconnect the account, including everything linked.
532   unlinkFirefoxAccount(confirm) {
533     window.browsingContext.topChromeWindow.gSync.disconnect({
534       confirm,
535     });
536   },
538   // Disconnect sync, leaving the account connected.
539   disconnectSync() {
540     return window.browsingContext.topChromeWindow.gSync.disconnect({
541       confirm: true,
542       disconnectAccount: false,
543     });
544   },
546   pairAnotherDevice() {
547     gSubDialog.open(
548       "chrome://browser/content/preferences/fxaPairDevice.xhtml",
549       { features: "resizable=no" }
550     );
551   },
553   _populateComputerName(value) {
554     let textbox = document.getElementById("fxaSyncComputerName");
555     if (!textbox.hasAttribute("placeholder")) {
556       textbox.setAttribute(
557         "placeholder",
558         fxAccounts.device.getDefaultLocalName()
559       );
560     }
561     textbox.value = value;
562   },
564   // arranges to dynamically show or hide sync engine name elements based on the
565   // preferences used for this engines.
566   setupEnginesUI() {
567     let observe = (elt, prefName) => {
568       elt.hidden = !Services.prefs.getBoolPref(prefName, false);
569     };
571     for (let elt of document.querySelectorAll("[engine_preference]")) {
572       let prefName = elt.getAttribute("engine_preference");
573       let obs = observe.bind(null, elt, prefName);
574       obs();
575       Services.prefs.addObserver(prefName, obs);
576       window.addEventListener("unload", () => {
577         Services.prefs.removeObserver(prefName, obs);
578       });
579     }
580   },