Mozilla Cross-Reference mozilla-central
mozilla/ browser/ base/ content/ browser-fxaccounts.js
Hg Log
Hg Blame
Diff file
Raw file
view using tree:
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
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 
5 let gFxAccounts = {
6 
7   PREF_SYNC_START_DOORHANGER: "services.sync.ui.showSyncStartDoorhanger",
8   DOORHANGER_ACTIVATE_DELAY_MS: 5000,
9   SYNC_MIGRATION_NOTIFICATION_TITLE: "fxa-migration",
10 
11   _initialized: false,
12   _inCustomizationMode: false,
13   // _expectingNotifyClose is a hack that helps us determine if the
14   // migration notification was closed due to being "dismissed" vs closed
15   // due to one of the migration buttons being clicked.  It's ugly and somewhat
16   // fragile, so bug 1119020 exists to help us do this better.
17   _expectingNotifyClose: false,
18 
19   get weave() {
20     delete this.weave;
21     return this.weave = Cc["@mozilla.org/weave/service;1"]
22                           .getService(Ci.nsISupports)
23                           .wrappedJSObject;
24   },
25 
26   get topics() {
27     // Do all this dance to lazy-load FxAccountsCommon.
28     delete this.topics;
29     return this.topics = [
30       "weave:service:ready",
31       "weave:service:sync:start",
32       "weave:service:login:error",
33       "weave:service:setup-complete",
34       "fxa-migration:state-changed",
35       this.FxAccountsCommon.ONVERIFIED_NOTIFICATION,
36       this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
37       "weave:notification:removed",
38     ];
39   },
40 
41   get button() {
42     delete this.button;
43     return this.button = document.getElementById("PanelUI-fxa-status");
44   },
45 
46   get strings() {
47     delete this.strings;
48     return this.strings = Services.strings.createBundle(
49       "chrome://browser/locale/accounts.properties"
50     );
51   },
52 
53   get loginFailed() {
54     // Referencing Weave.Service will implicitly initialize sync, and we don't
55     // want to force that - so first check if it is ready.
56     let service = Cc["@mozilla.org/weave/service;1"]
57                   .getService(Components.interfaces.nsISupports)
58                   .wrappedJSObject;
59     if (!service.ready) {
60       return false;
61     }
62     // LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
63     // All other login failures are assumed to be transient and should go
64     // away by themselves, so aren't reflected here.
65     return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
66   },
67 
68   get isActiveWindow() {
69     let fm = Services.focus;
70     return fm.activeWindow == window;
71   },
72 
73   init: function () {
74     // Bail out if we're already initialized and for pop-up windows.
75     if (this._initialized || !window.toolbar.visible) {
76       return;
77     }
78 
79     for (let topic of this.topics) {
80       Services.obs.addObserver(this, topic, false);
81     }
82 
83     addEventListener("activate", this);
84     gNavToolbox.addEventListener("customizationstarting", this);
85     gNavToolbox.addEventListener("customizationending", this);
86 
87     // Request the current Legacy-Sync-to-FxA migration status.  We'll be
88     // notified of fxa-migration:state-changed in response if necessary.
89     Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
90 
91     this._initialized = true;
92 
93     this.updateUI();
94   },
95 
96   uninit: function () {
97     if (!this._initialized) {
98       return;
99     }
100 
101     for (let topic of this.topics) {
102       Services.obs.removeObserver(this, topic);
103     }
104 
105     this._initialized = false;
106   },
107 
108   observe: function (subject, topic, data) {
109     switch (topic) {
110       case this.FxAccountsCommon.ONVERIFIED_NOTIFICATION:
111         Services.prefs.setBoolPref(this.PREF_SYNC_START_DOORHANGER, true);
112         break;
113       case "weave:service:sync:start":
114         this.onSyncStart();
115         break;
116       case "fxa-migration:state-changed":
117         this.onMigrationStateChanged(data, subject);
118         break;
119       case "weave:notification:removed":
120         // this exists just so we can tell the difference between "box was
121         // closed due to button press" vs "was closed due to click on [x]"
122         let notif = subject.wrappedJSObject.object;
123         if (notif.title == this.SYNC_MIGRATION_NOTIFICATION_TITLE &&
124             !this._expectingNotifyClose) {
125           // it's an [x] on our notification, so record telemetry.
126           this.fxaMigrator.recordTelemetry(this.fxaMigrator.TELEMETRY_DECLINED);
127         }
128         break;
129       default:
130         this.updateUI();
131         break;
132     }
133   },
134 
135   onSyncStart: function () {
136     if (!this.isActiveWindow) {
137       return;
138     }
139 
140     let showDoorhanger = false;
141 
142     try {
143       showDoorhanger = Services.prefs.getBoolPref(this.PREF_SYNC_START_DOORHANGER);
144     } catch (e) { /* The pref might not exist. */ }
145 
146     if (showDoorhanger) {
147       Services.prefs.clearUserPref(this.PREF_SYNC_START_DOORHANGER);
148       this.showSyncStartedDoorhanger();
149     }
150   },
151 
152   onMigrationStateChanged: function (newState, email) {
153     this._migrationInfo = !newState ? null : {
154       state: newState,
155       email: email ? email.QueryInterface(Ci.nsISupportsString).data : null,
156     };
157     this.updateUI();
158   },
159 
160   handleEvent: function (event) {
161     if (event.type == "activate") {
162       // Our window might have been in the background while we received the
163       // sync:start notification. If still needed, show the doorhanger after
164       // a short delay. Without this delay the doorhanger would not show up
165       // or with a too small delay show up while we're still animating the
166       // window.
167       setTimeout(() => this.onSyncStart(), this.DOORHANGER_ACTIVATE_DELAY_MS);
168     } else {
169       this._inCustomizationMode = event.type == "customizationstarting";
170       this.updateAppMenuItem();
171     }
172   },
173 
174   showDoorhanger: function (id) {
175     let panel = document.getElementById(id);
176     let anchor = document.getElementById("PanelUI-menu-button");
177 
178     let iconAnchor =
179       document.getAnonymousElementByAttribute(anchor, "class",
180                                               "toolbarbutton-icon");
181 
182     panel.hidden = false;
183     panel.openPopup(iconAnchor || anchor, "bottomcenter topright");
184   },
185 
186   showSyncStartedDoorhanger: function () {
187     this.showDoorhanger("sync-start-panel");
188   },
189 
190   showSyncFailedDoorhanger: function () {
191     this.showDoorhanger("sync-error-panel");
192   },
193 
194   updateUI: function () {
195     this.updateAppMenuItem();
196     this.updateMigrationNotification();
197   },
198 
199   updateAppMenuItem: function () {
200     if (this._migrationInfo) {
201       this.updateAppMenuItemForMigration();
202       return;
203     }
204 
205     // Bail out if FxA is disabled.
206     if (!this.weave.fxAccountsEnabled) {
207       // When migration transitions from needs-verification to the null state,
208       // fxAccountsEnabled is false because migration has not yet finished.  In
209       // that case, hide the button.  We'll get another notification with a null
210       // state once migration is complete.
211       this.button.hidden = true;
212       this.button.removeAttribute("fxastatus");
213       return;
214     }
215 
216     // FxA is enabled, show the widget.
217     this.button.hidden = false;
218 
219     // Make sure the button is disabled in customization mode.
220     if (this._inCustomizationMode) {
221       this.button.setAttribute("disabled", "true");
222     } else {
223       this.button.removeAttribute("disabled");
224     }
225 
226     let defaultLabel = this.button.getAttribute("defaultlabel");
227     let errorLabel = this.button.getAttribute("errorlabel");
228 
229     // If the user is signed into their Firefox account and we are not
230     // currently in customization mode, show their email address.
231     let doUpdate = userData => {
232       // Reset the button to its original state.
233       this.button.setAttribute("label", defaultLabel);
234       this.button.removeAttribute("tooltiptext");
235       this.button.removeAttribute("fxastatus");
236 
237       if (!this._inCustomizationMode) {
238         if (this.loginFailed) {
239           this.button.setAttribute("fxastatus", "error");
240           this.button.setAttribute("label", errorLabel);
241         } else if (userData) {
242           this.button.setAttribute("fxastatus", "signedin");
243           this.button.setAttribute("label", userData.email);
244           this.button.setAttribute("tooltiptext", userData.email);
245         }
246       }
247     }
248     fxAccounts.getSignedInUser().then(userData => {
249       doUpdate(userData);
250     }).then(null, error => {
251       // This is most likely in tests, were we quickly log users in and out.
252       // The most likely scenario is a user logged out, so reflect that.
253       // Bug 995134 calls for better errors so we could retry if we were
254       // sure this was the failure reason.
255       doUpdate(null);
256     });
257   },
258 
259   updateAppMenuItemForMigration: Task.async(function* () {
260     let status = null;
261     let label = null;
262     switch (this._migrationInfo.state) {
263       case this.fxaMigrator.STATE_USER_FXA:
264         status = "migrate-signup";
265         label = this.strings.formatStringFromName("needUserShort",
266           [this.button.getAttribute("fxabrandname")], 1);
267         break;
268       case this.fxaMigrator.STATE_USER_FXA_VERIFIED:
269         status = "migrate-verify";
270         label = this.strings.formatStringFromName("needVerifiedUserShort",
271                                                   [this._migrationInfo.email],
272                                                   1);
273         break;
274     }
275     this.button.label = label;
276     this.button.hidden = false;
277     this.button.setAttribute("fxastatus", status);
278   }),
279 
280   updateMigrationNotification: Task.async(function* () {
281     if (!this._migrationInfo) {
282       this._expectingNotifyClose = true;
283       Weave.Notifications.removeAll(this.SYNC_MIGRATION_NOTIFICATION_TITLE);
284       // because this is called even when there is no such notification, we
285       // set _expectingNotifyClose back to false as we may yet create a new
286       // notification (but in general, once we've created a migration
287       // notification once in a session, we don't create one again)
288       this._expectingNotifyClose = false;
289       return;
290     }
291     let note = null;
292     switch (this._migrationInfo.state) {
293       case this.fxaMigrator.STATE_USER_FXA: {
294         // There are 2 cases here - no email address means it is an offer on
295         // the first device (so the user is prompted to create an account).
296         // If there is an email address it is the "join the party" flow, so the
297         // user is prompted to sign in with the address they previously used.
298         let msg, upgradeLabel, upgradeAccessKey, learnMoreLink;
299         if (this._migrationInfo.email) {
300           msg = this.strings.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
301                                                   [this._migrationInfo.email],
302                                                   1);
303           upgradeLabel = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.label");
304           upgradeAccessKey = this.strings.GetStringFromName("signInAfterUpgradeOnOtherDevice.accessKey");
305         } else {
306           msg = this.strings.GetStringFromName("needUserLong");
307           upgradeLabel = this.strings.GetStringFromName("upgradeToFxA.label");
308           upgradeAccessKey = this.strings.GetStringFromName("upgradeToFxA.accessKey");
309           learnMoreLink = this.fxaMigrator.learnMoreLink;
310         }
311         note = new Weave.Notification(
312           undefined, msg, undefined, Weave.Notifications.PRIORITY_WARNING, [
313             new Weave.NotificationButton(upgradeLabel, upgradeAccessKey, () => {
314               this._expectingNotifyClose = true;
315               this.fxaMigrator.createFxAccount(window);
316             }),
317           ], learnMoreLink
318         );
319         break;
320       }
321       case this.fxaMigrator.STATE_USER_FXA_VERIFIED: {
322         let msg =
323           this.strings.formatStringFromName("needVerifiedUserLong",
324                                             [this._migrationInfo.email], 1);
325         let resendLabel =
326           this.strings.GetStringFromName("resendVerificationEmail.label");
327         let resendAccessKey =
328           this.strings.GetStringFromName("resendVerificationEmail.accessKey");
329         note = new Weave.Notification(
330           undefined, msg, undefined, Weave.Notifications.PRIORITY_INFO, [
331             new Weave.NotificationButton(resendLabel, resendAccessKey, () => {
332               this._expectingNotifyClose = true;
333               this.fxaMigrator.resendVerificationMail();
334             }),
335           ]
336         );
337         break;
338       }
339     }
340     note.title = this.SYNC_MIGRATION_NOTIFICATION_TITLE;
341     Weave.Notifications.replaceTitle(note);
342   }),
343 
344   onMenuPanelCommand: function (event) {
345     let button = event.originalTarget;
346 
347     switch (button.getAttribute("fxastatus")) {
348     case "signedin":
349       this.openPreferences();
350       break;
351     case "error":
352       this.openSignInAgainPage("menupanel");
353       break;
354     case "migrate-signup":
355     case "migrate-verify":
356       // The migration flow calls for the menu item to open sync prefs rather
357       // than requesting migration start immediately.
358       this.openPreferences();
359       break;
360     default:
361       this.openAccountsPage(null, { entryPoint: "menupanel" });
362       break;
363     }
364 
365     PanelUI.hide();
366   },
367 
368   openPreferences: function () {
369     openPreferences("paneSync");
370   },
371 
372   openAccountsPage: function (action, urlParams={}) {
373     // An entryPoint param is used for server-side metrics.  If the current tab
374     // is UITour, assume that it initiated the call to this method and override
375     // the entryPoint accordingly.
376     if (UITour.tourBrowsersByWindow.get(window) &&
377         UITour.tourBrowsersByWindow.get(window).has(gBrowser.selectedBrowser)) {
378       urlParams.entryPoint = "uitour";
379     }
380     let params = new URLSearchParams();
381     if (action) {
382       params.set("action", action);
383     }
384     for (let name in urlParams) {
385       if (urlParams[name] !== undefined) {
386         params.set(name, urlParams[name]);
387       }
388     }
389     let url = "about:accounts?" + params;
390     switchToTabHavingURI(url, true, {
391       replaceQueryString: true
392     });
393   },
394 
395   openSignInAgainPage: function (entryPoint) {
396     this.openAccountsPage("reauth", { entryPoint: entryPoint });
397   },
398 };
399 
400 XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function () {
401   return Cu.import("resource://gre/modules/FxAccountsCommon.js", {});
402 });
403 
404 XPCOMUtils.defineLazyModuleGetter(gFxAccounts, "fxaMigrator",
405   "resource://services-sync/FxaMigrator.jsm");
406 
view http://hg.mozilla.org/mozilla-central/rev/ /browser/base/content/browser-fxaccounts.js